Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d00b3c932 | ||
|
|
a380b5e8ae | ||
|
|
9ff5024a58 | ||
|
|
d6dd082e59 | ||
|
|
0531f30590 | ||
|
|
27ca0cd21e | ||
|
|
920a898955 | ||
|
|
7cbc974763 | ||
|
|
c71b764546 | ||
|
|
5db8430a73 | ||
|
|
091c4a2ac1 | ||
|
|
8e935d08a6 | ||
|
|
b959c55852 | ||
|
|
bab9b21882 | ||
|
|
703fbf5c8a | ||
|
|
aaaf855a6e | ||
|
|
a6666be477 | ||
|
|
ba90b04624 | ||
|
|
debbdde5d1 | ||
|
|
48f62f541f | ||
|
|
e6296e4ce4 | ||
|
|
423650e8b5 | ||
|
|
d0c42e37a7 | ||
|
|
6afd17f1ed | ||
|
|
cb0eec2203 | ||
|
|
f5900b726b | ||
|
|
0d9e777c18 | ||
|
|
f5cc61c211 | ||
|
|
786ba43b83 | ||
|
|
ee80cfa27f | ||
|
|
bbaca64d36 | ||
|
|
b608ee3985 | ||
|
|
5e72e28ba6 | ||
|
|
84b18c1a7b | ||
|
|
249851a556 | ||
|
|
344b4c8d3d | ||
|
|
0dd314b53c | ||
|
|
e9f51526f6 | ||
|
|
ae2080fd9d | ||
|
|
8f6ebac2ce | ||
|
|
23d67677c6 | ||
|
|
00d64556bc | ||
|
|
70c0accf7f | ||
|
|
82ae4cb10f | ||
|
|
99d3cc87b2 | ||
|
|
232d6f39e4 | ||
|
|
26089f0886 | ||
|
|
a1de95a8ce | ||
|
|
f4861bf3c1 | ||
|
|
7fb3b2f591 | ||
|
|
0dec68294f | ||
|
|
69ba548dab | ||
|
|
cfa9ff15bb | ||
|
|
ddf7441152 | ||
|
|
20b4a3fb84 | ||
|
|
81f4420a67 |
+3
-3
@@ -3,12 +3,12 @@ kind: pipeline
|
||||
name: coopcloud.tech/abra
|
||||
steps:
|
||||
- name: make check
|
||||
image: golang:1.26
|
||||
image: golang:1.27
|
||||
commands:
|
||||
- make check
|
||||
|
||||
- name: xgettext-go
|
||||
image: golang:1.26
|
||||
image: golang:1.27
|
||||
environment:
|
||||
GOPRIVATE: coopcloud.tech
|
||||
commands:
|
||||
@@ -43,7 +43,7 @@ steps:
|
||||
- tag
|
||||
|
||||
- name: make test
|
||||
image: golang:1.26
|
||||
image: golang:1.27
|
||||
environment:
|
||||
ABRA_DIR: /root/.abra_test
|
||||
commands:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
vendor/** linguist-generated=true
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Build image
|
||||
FROM golang:1.26-alpine AS build
|
||||
FROM golang:1.27-alpine AS build
|
||||
|
||||
ENV GOPRIVATE=coopcloud.tech
|
||||
|
||||
|
||||
@@ -13,14 +13,14 @@ require (
|
||||
github.com/docker/cli v28.4.0+incompatible
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/go-git/go-git/v5 v5.19.1
|
||||
github.com/go-git/go-git/v5 v5.19.2
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/leonelquinteros/gotext v1.7.2
|
||||
github.com/moby/sys/signal v0.7.1
|
||||
github.com/moby/term v0.5.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/schollz/progressbar/v3 v3.19.1
|
||||
golang.org/x/term v0.44.0
|
||||
golang.org/x/term v0.46.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gotest.tools/v3 v3.5.2
|
||||
)
|
||||
@@ -50,7 +50,6 @@ require (
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
@@ -97,7 +96,6 @@ require (
|
||||
github.com/opencontainers/runc v1.1.13 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
@@ -123,11 +121,11 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/crypto v0.53.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/text v0.39.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
@@ -152,10 +150,10 @@ require (
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
golang.org/x/sys v0.46.0
|
||||
golang.org/x/sys v0.48.0
|
||||
)
|
||||
|
||||
replace github.com/docker/cli v28.4.0+incompatible => git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible
|
||||
|
||||
@@ -306,7 +306,6 @@ github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW
|
||||
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
|
||||
github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decentral1se/cobra v1.10.2 h1:MZ8Ifi/jRels9sZrpSccDbUlK++3b2HlBODfv0Bh6x0=
|
||||
github.com/decentral1se/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
@@ -393,8 +392,8 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm
|
||||
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
||||
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
|
||||
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -760,7 +759,6 @@ github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
@@ -857,8 +855,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
|
||||
@@ -946,8 +944,9 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -966,8 +965,8 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1043,8 +1042,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1139,13 +1138,13 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1155,8 +1154,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
|
||||
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
||||
@@ -52,7 +52,6 @@ func Clone(dir, url string) error {
|
||||
URL: url,
|
||||
Tags: git.AllTags,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/main"),
|
||||
SingleBranch: true,
|
||||
})
|
||||
|
||||
if err != nil && gitCloneIgnoreErr(err) {
|
||||
@@ -71,7 +70,6 @@ func Clone(dir, url string) error {
|
||||
URL: url,
|
||||
Tags: git.AllTags,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
|
||||
SingleBranch: true,
|
||||
})
|
||||
|
||||
if err != nil && gitCloneIgnoreErr(err) {
|
||||
|
||||
+36
-36
@@ -7,7 +7,7 @@
|
||||
msgid ""
|
||||
msgstr "Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: EMAIL\n"
|
||||
"POT-Creation-Date: 2026-06-14 17:56+0200\n"
|
||||
"POT-Creation-Date: 2026-08-31 19:21+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -189,7 +189,7 @@ msgstr ""
|
||||
msgid "%d volumes removed successfully"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:197
|
||||
#: ./pkg/recipe/git.go:198
|
||||
#, c-format
|
||||
msgid "%s (%s) has locally unstaged changes?"
|
||||
msgstr ""
|
||||
@@ -419,7 +419,7 @@ msgstr ""
|
||||
msgid "%s sanitised as %s for new app"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:445
|
||||
#: ./pkg/recipe/git.go:451
|
||||
#, c-format
|
||||
msgid "%s service is missing image tag?"
|
||||
msgstr ""
|
||||
@@ -902,7 +902,7 @@ msgstr ""
|
||||
msgid "Generate the recipe catalogue"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:459
|
||||
#: ./pkg/recipe/git.go:465
|
||||
#, c-format
|
||||
msgid "GetRecipeVersions encountered error for %s: %s (collected %d versions)"
|
||||
msgstr ""
|
||||
@@ -1815,7 +1815,7 @@ msgstr ""
|
||||
msgid "are you sure?"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:162
|
||||
#: ./pkg/recipe/git.go:163
|
||||
#, c-format
|
||||
msgid "attempting to checkout '%s' as chaos commit"
|
||||
msgstr ""
|
||||
@@ -1951,7 +1951,7 @@ msgstr ""
|
||||
msgid "cancelled"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/catalogue/catalogue.go:59 ./pkg/recipe/git.go:251
|
||||
#: ./pkg/catalogue/catalogue.go:59 ./pkg/recipe/git.go:252
|
||||
#, c-format
|
||||
msgid "cannot ensure %s is up-to-date, no git remotes configured"
|
||||
msgstr ""
|
||||
@@ -1966,7 +1966,7 @@ msgstr ""
|
||||
msgid "cannot get label %s for %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:58
|
||||
#: ./pkg/recipe/git.go:59
|
||||
#, c-format
|
||||
msgid "cannot redeploy previous chaos version (%s), did you mean to use \"--chaos\"?"
|
||||
msgstr ""
|
||||
@@ -2118,7 +2118,7 @@ msgstr ""
|
||||
msgid "cmd"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:470
|
||||
#: ./pkg/recipe/git.go:476
|
||||
#, c-format
|
||||
msgid "collected %s for %s"
|
||||
msgstr ""
|
||||
@@ -2456,7 +2456,7 @@ msgstr ""
|
||||
msgid "destination directory does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:373
|
||||
#: ./pkg/recipe/git.go:379
|
||||
#, c-format
|
||||
msgid "detected %s as tags for recipe %s"
|
||||
msgstr ""
|
||||
@@ -2653,7 +2653,7 @@ msgstr ""
|
||||
msgid "ensure recipe: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:56
|
||||
#: ./pkg/recipe/git.go:57
|
||||
#, c-format
|
||||
msgid "ensuring env version %s"
|
||||
msgstr ""
|
||||
@@ -2751,12 +2751,12 @@ msgstr ""
|
||||
msgid "failed to check git status of %s: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/git/branch.go:95 ./pkg/recipe/git.go:231
|
||||
#: ./pkg/git/branch.go:95 ./pkg/recipe/git.go:232
|
||||
#, c-format
|
||||
msgid "failed to check out %s in %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:412
|
||||
#: ./pkg/recipe/git.go:418
|
||||
#, c-format
|
||||
msgid "failed to check out %s in %s: %s"
|
||||
msgstr ""
|
||||
@@ -2806,7 +2806,7 @@ msgstr ""
|
||||
msgid "failed to generate random bytes: %w"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:421
|
||||
#: ./pkg/recipe/git.go:427
|
||||
#, c-format
|
||||
msgid "failed to get compose config for %s: %s"
|
||||
msgstr ""
|
||||
@@ -2844,7 +2844,7 @@ msgstr ""
|
||||
msgid "failed to parse image %s, saw: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:431
|
||||
#: ./pkg/recipe/git.go:437
|
||||
#, c-format
|
||||
msgid "failed to parse image for %s in %s: %s"
|
||||
msgstr ""
|
||||
@@ -2952,7 +2952,7 @@ msgstr ""
|
||||
msgid "fetch all recipes"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/catalogue/catalogue.go:84 ./pkg/recipe/git.go:284
|
||||
#: ./pkg/catalogue/catalogue.go:84 ./pkg/recipe/git.go:290
|
||||
#, c-format
|
||||
msgid "fetched latest git changes for %s"
|
||||
msgstr ""
|
||||
@@ -3070,12 +3070,12 @@ msgstr ""
|
||||
msgid "git changes pushed"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:417
|
||||
#: ./pkg/recipe/git.go:423
|
||||
#, c-format
|
||||
msgid "git checkout: %s in %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/git/clone.go:64 ./pkg/git/clone.go:102
|
||||
#: ./pkg/git/clone.go:63 ./pkg/git/clone.go:100
|
||||
#, c-format
|
||||
msgid "git clone %s: cancelled due to interrupt"
|
||||
msgstr ""
|
||||
@@ -3085,17 +3085,17 @@ msgstr ""
|
||||
msgid "git clone: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/git/clone.go:89
|
||||
#: ./pkg/git/clone.go:87
|
||||
#, c-format
|
||||
msgid "git clone: %s already exists"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/git/clone.go:59 ./pkg/git/clone.go:78 ./pkg/git/clone.go:87
|
||||
#: ./pkg/git/clone.go:58 ./pkg/git/clone.go:76 ./pkg/git/clone.go:85
|
||||
#, c-format
|
||||
msgid "git clone: %s cloned successfully"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/git/clone.go:68
|
||||
#: ./pkg/git/clone.go:67
|
||||
msgid "git clone: main branch failed, attempting master branch"
|
||||
msgstr ""
|
||||
|
||||
@@ -3150,7 +3150,7 @@ msgstr ""
|
||||
msgid "git.coopcloud.tech repo exists"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:384
|
||||
#: ./pkg/recipe/git.go:390
|
||||
#, c-format
|
||||
msgid "git: opening repository in %s"
|
||||
msgstr ""
|
||||
@@ -4061,7 +4061,7 @@ msgstr ""
|
||||
msgid "proceed?"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:404
|
||||
#: ./pkg/recipe/git.go:410
|
||||
#, c-format
|
||||
msgid "processing %s for %s"
|
||||
msgstr ""
|
||||
@@ -4129,7 +4129,7 @@ msgstr ""
|
||||
msgid "re"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:157
|
||||
#: ./pkg/recipe/git.go:158
|
||||
#, c-format
|
||||
msgid "read %s as tags for recipe %s"
|
||||
msgstr ""
|
||||
@@ -4865,17 +4865,17 @@ msgstr ""
|
||||
msgid "skipping secret (because it already exists) on %s: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:413
|
||||
#: ./pkg/recipe/git.go:419
|
||||
#, c-format
|
||||
msgid "skipping tag %s: checkout failed: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:422
|
||||
#: ./pkg/recipe/git.go:428
|
||||
#, c-format
|
||||
msgid "skipping tag %s: invalid compose config: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:432
|
||||
#: ./pkg/recipe/git.go:438
|
||||
#, c-format
|
||||
msgid "skipping tag %s: invalid image reference in service %s: %s"
|
||||
msgstr ""
|
||||
@@ -4978,7 +4978,7 @@ msgstr ""
|
||||
msgid "succeeded"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:184
|
||||
#: ./pkg/recipe/git.go:185
|
||||
#, c-format
|
||||
msgid "successfully checked %s out to %s in %s"
|
||||
msgstr ""
|
||||
@@ -5138,17 +5138,17 @@ msgstr ""
|
||||
msgid "un"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:193
|
||||
#: ./pkg/recipe/git.go:194
|
||||
#, c-format
|
||||
msgid "unable to check git clean status in %s: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:262
|
||||
#: ./pkg/recipe/git.go:263
|
||||
#, c-format
|
||||
msgid "unable to check out default branch in %s: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/git/clone.go:100
|
||||
#: ./pkg/git/clone.go:98
|
||||
#, c-format
|
||||
msgid "unable to clean up git clone of %s: %s"
|
||||
msgstr ""
|
||||
@@ -5222,7 +5222,7 @@ msgstr ""
|
||||
msgid "unable to discover SSH remote for %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:268
|
||||
#: ./pkg/recipe/git.go:274
|
||||
#, c-format
|
||||
msgid "unable to fetch tags in %s: %s"
|
||||
msgstr ""
|
||||
@@ -5232,7 +5232,7 @@ msgstr ""
|
||||
msgid "unable to get container matching %s: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:280
|
||||
#: ./pkg/recipe/git.go:286
|
||||
#, c-format
|
||||
msgid "unable to git pull in %s: %s"
|
||||
msgstr ""
|
||||
@@ -5261,12 +5261,12 @@ msgstr ""
|
||||
msgid "unable to look up server context for %s: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/fetch.go:77 ./pkg/git/read.go:26 ./pkg/lint/recipe.go:491 ./pkg/recipe/git.go:242
|
||||
#: ./cli/recipe/fetch.go:77 ./pkg/git/read.go:26 ./pkg/lint/recipe.go:491 ./pkg/recipe/git.go:243
|
||||
#, c-format
|
||||
msgid "unable to open %s: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:257
|
||||
#: ./pkg/recipe/git.go:258
|
||||
#, c-format
|
||||
msgid "unable to open git work tree in %s: %s"
|
||||
msgstr ""
|
||||
@@ -5326,7 +5326,7 @@ msgstr ""
|
||||
msgid "unable to read new env %s: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:247
|
||||
#: ./pkg/recipe/git.go:248
|
||||
#, c-format
|
||||
msgid "unable to read remotes in %s: %s"
|
||||
msgstr ""
|
||||
@@ -5361,7 +5361,7 @@ msgstr ""
|
||||
msgid "unable to reset commit after failed release attempt: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/git.go:166
|
||||
#: ./pkg/recipe/git.go:167
|
||||
#, c-format
|
||||
msgid "unable to resolve '%s': %s"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
+143
-81
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: EMAIL\n"
|
||||
"POT-Creation-Date: 2026-06-14 17:56+0200\n"
|
||||
"PO-Revision-Date: 2026-02-28 13:52+0000\n"
|
||||
"POT-Creation-Date: 2026-08-31 19:21+0000\n"
|
||||
"PO-Revision-Date: 2026-09-01 22:35+0000\n"
|
||||
"Last-Translator: chasqui <chasqui@cryptolab.net>\n"
|
||||
"Language-Team: Spanish <https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/>\n"
|
||||
"Language: es\n"
|
||||
@@ -113,17 +113,25 @@ msgid ""
|
||||
" # list apps of all servers which match a specific recipe\n"
|
||||
" abra app ls -r gitea"
|
||||
msgstr ""
|
||||
" # Listar las aplicaciones instaladas\n"
|
||||
" abra aplicacion listar\n"
|
||||
" OPCIONES Y FLAGS\n"
|
||||
" ────────────────\n"
|
||||
" ejemplos de uso:\n"
|
||||
"\n"
|
||||
" # Listar las aplicaciones instaladas en un servidor específico\n"
|
||||
" abra aplicacion listar -s 1312.net\n"
|
||||
"┌─ Información detallada ─────────────────────────────────────┐\n"
|
||||
"│ $ abra aplicacion listar --estado │\n"
|
||||
"└─────────────────────────────────────────────────────────────┘\n"
|
||||
"\n"
|
||||
" # Listar aplicaciones instaladas en servidor específico con detalles\n"
|
||||
" abra aplicacion listar -s 1312.net -S\n"
|
||||
"┌─ Aplicaciones de una receta ────────────────────────────────┐\n"
|
||||
"│ $ abra aplicacion listar --receta wordpress │\n"
|
||||
"└─────────────────────────────────────────────────────────────┘\n"
|
||||
"\n"
|
||||
" # Listar en qué servidores está desplegada la aplicación \"gitea\"\n"
|
||||
" abra aplicacion listar -r gitea"
|
||||
"┌─ Receta + información detallada ────────────────────────────┐\n"
|
||||
"│ $ abra aplicacion listar --receta wordpress --estado │\n"
|
||||
"└─────────────────────────────────────────────────────────────┘\n"
|
||||
"\n"
|
||||
"┌─ Forma abreviada ───────────────────────────────────────────┐\n"
|
||||
"│ $ abra app ls -S │\n"
|
||||
"└─────────────────────────────────────────────────────────────┘"
|
||||
|
||||
#: cli/app/move.go:59
|
||||
msgid ""
|
||||
@@ -236,7 +244,21 @@ msgstr " abra aplicacion entorno listar \"nombre_app.dominio\""
|
||||
|
||||
#: cli/app/remove.go:45
|
||||
msgid " abra app remove 1312.net"
|
||||
msgstr " abra aplicacion borrar \"nombre_app.dominio\""
|
||||
msgstr ""
|
||||
" EJEMPLOS DE USO\n"
|
||||
" ────────────────\n"
|
||||
"\n"
|
||||
"┌─ Borrar una aplicación ─────────────────────────────────────┐\n"
|
||||
"│ $ abra aplicacion borrar \"nombre_app.dominio\" │\n"
|
||||
"└─────────────────────────────────────────────────────────────┘\n"
|
||||
"\n"
|
||||
"┌─ Borrar sin confirmación ───────────────────────────────────┐\n"
|
||||
"│ $ abra aplicacion borrar \"nombre_app.dominio\" --forzar │\n"
|
||||
"└─────────────────────────────────────────────────────────────┘\n"
|
||||
"\n"
|
||||
"┌─ Borrar sin confirmación usando comando abreviado ──────────┐\n"
|
||||
"│ $ abra app rm \"nombre_app.dominio\" -f │\n"
|
||||
"└─────────────────────────────────────────────────────────────┘"
|
||||
|
||||
#: cli/app/secret.go:346
|
||||
msgid " abra app secret rm 1312.net oauth_key"
|
||||
@@ -292,7 +314,7 @@ msgstr "%d volumen(es) eliminados con éxito"
|
||||
msgid "%d volumes removed successfully"
|
||||
msgstr "%d volúmenes eliminados correctamente"
|
||||
|
||||
#: pkg/recipe/git.go:197
|
||||
#: pkg/recipe/git.go:198
|
||||
#, c-format
|
||||
msgid "%s (%s) has locally unstaged changes?"
|
||||
msgstr "%s (%s) ¿tiene cambios locales?"
|
||||
@@ -513,7 +535,7 @@ msgstr "%s eliminado del almacén de contraseñas"
|
||||
#: cli/app/new.go:224
|
||||
#, c-format
|
||||
msgid "%s requires secret generation before deploy, run \"abra app secret generate %s --all\""
|
||||
msgstr "%s requiere generación de secretos antes del despliegue, ejecuta \"abra aplicacion secreto generar %s --all\""
|
||||
msgstr "%s requiere generación de secretos antes del despliegue, ejecuta \"abra aplicacion secreto generar %s --todos\""
|
||||
|
||||
#: cli/app/new.go:228
|
||||
#, c-format
|
||||
@@ -525,7 +547,7 @@ msgstr "%s requiere inserción de secretos antes del despliegue (#generate=false
|
||||
msgid "%s sanitised as %s for new app"
|
||||
msgstr "%s sanitisado como %s para nueva aplicación"
|
||||
|
||||
#: pkg/recipe/git.go:445
|
||||
#: pkg/recipe/git.go:451
|
||||
#, c-format
|
||||
msgid "%s service is missing image tag?"
|
||||
msgstr "¿falta la etiqueta de imagen del servicio %s?"
|
||||
@@ -1108,9 +1130,25 @@ msgid ""
|
||||
"\n"
|
||||
"Use \"--status/-S\" flag to query all servers for the live deployment status."
|
||||
msgstr ""
|
||||
"Genera un informe de todas las aplicaciones gestionadas.\n"
|
||||
"┌──────────────────────────────────────────────────────────────┐\n"
|
||||
"│ MANUAL DE USO · COMANDO «LISTAR» │\n"
|
||||
"└──────────────────────────────────────────────────────────────┘\n"
|
||||
" DESCRIPCIÓN\n"
|
||||
" ───────────\n"
|
||||
" El comando:\n"
|
||||
"\n"
|
||||
"Usa la opción \"--estado/-S\" para consultar en todos los servidores sobre el estado de despliegue."
|
||||
" abra aplicacion listar\n"
|
||||
"\n"
|
||||
"Permite consultar rápidamente qué aplicaciones están siendo\n"
|
||||
"gestionadas por Abra.\n"
|
||||
"\n"
|
||||
"Al ejecutarlo, Abra genera un informe con la lista de\n"
|
||||
"aplicaciones disponibles y la información básica de cada una.\n"
|
||||
"\n"
|
||||
"En pocas palabras, puedes imaginar `listar` como una forma\n"
|
||||
"de decirle a Abra:\n"
|
||||
"\n"
|
||||
" «muéstrame qué aplicaciones tengo»"
|
||||
|
||||
#: cli/app/new.go:321
|
||||
msgid "Generate app secrets?"
|
||||
@@ -1136,7 +1174,7 @@ msgstr "Genera 🥷 secretos (contraseñas) automáticamente"
|
||||
msgid "Generate the recipe catalogue"
|
||||
msgstr "📋 Muestra el catálogo de 📜 recetas"
|
||||
|
||||
#: pkg/recipe/git.go:459
|
||||
#: pkg/recipe/git.go:465
|
||||
#, c-format
|
||||
msgid "GetRecipeVersions encountered error for %s: %s (collected %d versions)"
|
||||
msgstr "GetRecipeVersions encontró un error para %s: %s (recopiladas %d versiones)"
|
||||
@@ -1478,24 +1516,48 @@ msgid ""
|
||||
"To delete everything without prompt, use the \"--force/-f\" or the \"--no-input/n\"\n"
|
||||
"flag."
|
||||
msgstr ""
|
||||
"Elimina todo lo relacionado con una aplicación que ya está plegada.\n"
|
||||
"┌──────────────────────────────────────────────────────────────┐\n"
|
||||
"│ MANUAL DE USO · COMANDO «BORRAR» │\n"
|
||||
"└──────────────────────────────────────────────────────────────┘\n"
|
||||
" DESCRIPCIÓN\n"
|
||||
" ───────────\n"
|
||||
" El comando:\n"
|
||||
"\n"
|
||||
"Por defecto, pedirá confirmación antes de continuar. Se borrarán \n"
|
||||
"todos los secretos, volúmenes y el archivo .env local de la aplicación.\n"
|
||||
" abra aplicacion borrar \"nombre_app.dominio\"\n"
|
||||
"\n"
|
||||
"Ejecuta este comando solo si estás seguro de querer quitar por \n"
|
||||
"completo la aplicación y todos sus datos asociados. Esta acción \n"
|
||||
"es destructiva. ¡Ten cuidado!\n"
|
||||
"elimina todo lo relacionado con una aplicación que ya está\n"
|
||||
"desplegada.\n"
|
||||
"\n"
|
||||
"Si quieres borrar volúmenes o secretos específicos, \n"
|
||||
"usa los subcomandos de eliminación bajo \"aplicación volumen\" \n"
|
||||
"y \"aplicación secreto\" en su lugar.\n"
|
||||
"Por defecto, Abra pedirá confirmación antes de continuar.\n"
|
||||
"\n"
|
||||
"Ten en cuenta que si borras el archivo .env local sin eliminar primero \n"
|
||||
"volúmenes y secretos, Abra *no* podrá ayudarte a eliminarlos después.\n"
|
||||
"La eliminación incluye:\n"
|
||||
"\n"
|
||||
"Para borrar todo sin confirmación, usa la opción \"--forzar/-f\" \n"
|
||||
"o la opción \"--no-input/-n\"."
|
||||
" ├── secretos\n"
|
||||
" ├── volúmenes\n"
|
||||
" └── archivo .env local\n"
|
||||
"\n"
|
||||
"\n"
|
||||
" ¡ATENCIÓN!\n"
|
||||
" ──────────\n"
|
||||
"Esta es una acción destructiva.\n"
|
||||
"\n"
|
||||
"La aplicación y todos los datos asociados serán eliminados.\n"
|
||||
"Ejecuta este comando únicamente si estás seguro de querer\n"
|
||||
"quitar la aplicación por completo.\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"Si quieres eliminar volúmenes o secretos específicos, utiliza\n"
|
||||
"los subcomandos correspondientes:\n"
|
||||
"\n"
|
||||
" abra aplicacion volumen\n"
|
||||
" abra aplicacion secreto\n"
|
||||
"\n"
|
||||
"\n"
|
||||
" IMPORTANTE\n"
|
||||
" ──────────\n"
|
||||
"Si eliminas el archivo .env local sin eliminar primero los\n"
|
||||
"volúmenes y secretos, Abra no podrá ayudarte a eliminarlos\n"
|
||||
"después."
|
||||
|
||||
#. translators: Short description for `app volume remove` command
|
||||
#: cli/app/volume.go:90
|
||||
@@ -2025,32 +2087,32 @@ msgid ""
|
||||
"\n"
|
||||
"Use \"{{.CommandPath}} [command] --help\" for more information about a command.{{end}}\n"
|
||||
msgstr ""
|
||||
"Uso:{{if .Runnable}}\n"
|
||||
" {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}\n"
|
||||
" {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}\n"
|
||||
"\n"
|
||||
"Comando:\n"
|
||||
" {{.NameAndAliases}}{{end}}{{if .HasExample}}\n"
|
||||
"\n"
|
||||
"Ejemplos:\n"
|
||||
" # Nota: \"1312.net\" es solo un ejemplo de nombre de aplicación.\n"
|
||||
"# Reemplázalo por el dominio o nombre real de tu aplicación.\n"
|
||||
"COMANDO ABREVIADO\n"
|
||||
"─────────────────\n"
|
||||
" {{range .Aliases}}{{.}}{{end}}\n"
|
||||
"\n"
|
||||
"{{if .HasExample}}\n"
|
||||
"{{.Example}}{{end}}{{if .HasAvailableSubCommands}}\n"
|
||||
"\n"
|
||||
"Comandos disponibles:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name \"help\"))}}\n"
|
||||
" {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\n"
|
||||
"\n"
|
||||
"Opciones:\n"
|
||||
"{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\n"
|
||||
"\n"
|
||||
"Opciones globales:\n"
|
||||
"{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}\n"
|
||||
" {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasHelpSubCommands}}\n"
|
||||
"\n"
|
||||
"Temas de ayuda adicionales:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}\n"
|
||||
" {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}\n"
|
||||
" {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}\n"
|
||||
"\n"
|
||||
"Use \"{{.CommandPath}} [command] --help\" para más información sobre un comando.{{end}}\n"
|
||||
" SINTAXIS\n"
|
||||
" ────────{{if .Runnable}}\n"
|
||||
" {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}\n"
|
||||
" {{.CommandPath}} [command]{{end}}\n"
|
||||
"\n"
|
||||
"Opciones:{{if .HasAvailableLocalFlags}}\n"
|
||||
"{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\n"
|
||||
"\n"
|
||||
"Opciones globales:{{if .HasAvailableInheritedFlags}}\n"
|
||||
"{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\n"
|
||||
"\n"
|
||||
"{{if .HasAvailableSubCommands}}\n"
|
||||
"Use \"{{.CommandPath}} [comando] --help\" para más información sobre un comando.{{end}}\n"
|
||||
|
||||
#: cli/recipe/fetch.go:28
|
||||
msgid "Using \"--force/-f\" Git syncs an existing recipe. It does not erase unstaged changes."
|
||||
@@ -2348,7 +2410,7 @@ msgstr "¿A la aplicación le falta la etiqueta de imagen?"
|
||||
msgid "are you sure?"
|
||||
msgstr "¿Estás segurx?"
|
||||
|
||||
#: pkg/recipe/git.go:162
|
||||
#: pkg/recipe/git.go:163
|
||||
#, c-format
|
||||
msgid "attempting to checkout '%s' as chaos commit"
|
||||
msgstr "Intentando extraer '%s' como el commit de caos"
|
||||
@@ -2491,7 +2553,7 @@ msgstr "no se pueden leer las recetas locales: %s"
|
||||
msgid "cancelled"
|
||||
msgstr "cancelado"
|
||||
|
||||
#: pkg/catalogue/catalogue.go:59 pkg/recipe/git.go:251
|
||||
#: pkg/catalogue/catalogue.go:59 pkg/recipe/git.go:252
|
||||
#, c-format
|
||||
msgid "cannot ensure %s is up-to-date, no git remotes configured"
|
||||
msgstr "no se puede asegurar que %s esté actualizado, no hay remotos de git configurados"
|
||||
@@ -2506,7 +2568,7 @@ msgstr "no se puede encontrar la aplicación con el nombre %s"
|
||||
msgid "cannot get label %s for %s"
|
||||
msgstr "no se puede obtener la etiqueta %s para %s"
|
||||
|
||||
#: pkg/recipe/git.go:58
|
||||
#: pkg/recipe/git.go:59
|
||||
#, c-format
|
||||
msgid "cannot redeploy previous chaos version (%s), did you mean to use \"--chaos\"?"
|
||||
msgstr "no se puede redeplegar la versión anterior de caos (%s), ¿Era tu intención usar \"--caos\"?"
|
||||
@@ -2666,7 +2728,7 @@ msgstr "tarea: Actualizar tags de una imagen"
|
||||
msgid "cmd"
|
||||
msgstr ""
|
||||
|
||||
#: pkg/recipe/git.go:470
|
||||
#: pkg/recipe/git.go:476
|
||||
#, c-format
|
||||
msgid "collected %s for %s"
|
||||
msgstr "colectado %s para %s"
|
||||
@@ -3005,7 +3067,7 @@ msgstr "tiempo de espera agotado 🟠"
|
||||
msgid "destination directory does not exist"
|
||||
msgstr "el directorio de destino no existe"
|
||||
|
||||
#: pkg/recipe/git.go:373
|
||||
#: pkg/recipe/git.go:379
|
||||
#, c-format
|
||||
msgid "detected %s as tags for recipe %s"
|
||||
msgstr "detectado(s) %s como etiquetas para la receta %s"
|
||||
@@ -3202,7 +3264,7 @@ msgstr "asegúrate que \"image: ...\" esté establecido en todos los servicios"
|
||||
msgid "ensure recipe: %s"
|
||||
msgstr "asegura receta: %s"
|
||||
|
||||
#: pkg/recipe/git.go:56
|
||||
#: pkg/recipe/git.go:57
|
||||
#, c-format
|
||||
msgid "ensuring env version %s"
|
||||
msgstr "asegurando la versión del entorno (env) %s"
|
||||
@@ -3304,12 +3366,12 @@ msgstr "🛑 error al agregar notas de la versión: %s"
|
||||
msgid "failed to check git status of %s: %s"
|
||||
msgstr "🛑 error al comprobar el estado de git de %s: %s"
|
||||
|
||||
#: pkg/git/branch.go:95 pkg/recipe/git.go:231
|
||||
#: pkg/git/branch.go:95 pkg/recipe/git.go:232
|
||||
#, c-format
|
||||
msgid "failed to check out %s in %s"
|
||||
msgstr "🛑 error al cambiar a %s en %s"
|
||||
|
||||
#: pkg/recipe/git.go:412
|
||||
#: pkg/recipe/git.go:418
|
||||
#, c-format
|
||||
msgid "failed to check out %s in %s: %s"
|
||||
msgstr "🛑 error al cambiar a %s en %s: %s"
|
||||
@@ -3359,7 +3421,7 @@ msgstr "🛑 error al crear el volumen %s en %s: %s"
|
||||
msgid "failed to generate random bytes: %w"
|
||||
msgstr "🛑 error al generar bytes aleatorios: %w"
|
||||
|
||||
#: pkg/recipe/git.go:421
|
||||
#: pkg/recipe/git.go:427
|
||||
#, c-format
|
||||
msgid "failed to get compose config for %s: %s"
|
||||
msgstr "🛑 error al obtener la configuración para %s: %s"
|
||||
@@ -3397,7 +3459,7 @@ msgstr "🛑 error al migrar la configuración de la aplicación: %s"
|
||||
msgid "failed to parse image %s, saw: %s"
|
||||
msgstr "🛑 error al analizar la imagen %s; detectado: %s"
|
||||
|
||||
#: pkg/recipe/git.go:431
|
||||
#: pkg/recipe/git.go:437
|
||||
#, c-format
|
||||
msgid "failed to parse image for %s in %s: %s"
|
||||
msgstr "🛑 error al analizar la imagen %s; en: %s: %s"
|
||||
@@ -3505,7 +3567,7 @@ msgstr "obtener [receta | --todos] [opciones]"
|
||||
msgid "fetch all recipes"
|
||||
msgstr "Obtener todas las recetas"
|
||||
|
||||
#: pkg/catalogue/catalogue.go:84 pkg/recipe/git.go:284
|
||||
#: pkg/catalogue/catalogue.go:84 pkg/recipe/git.go:290
|
||||
#, c-format
|
||||
msgid "fetched latest git changes for %s"
|
||||
msgstr "Se obtuvieron los últimos cambios de Git para %s"
|
||||
@@ -3625,12 +3687,12 @@ msgstr "cambios de git confirmados"
|
||||
msgid "git changes pushed"
|
||||
msgstr "cambios de git enviados (push)"
|
||||
|
||||
#: pkg/recipe/git.go:417
|
||||
#: pkg/recipe/git.go:423
|
||||
#, c-format
|
||||
msgid "git checkout: %s in %s"
|
||||
msgstr ""
|
||||
|
||||
#: pkg/git/clone.go:64 pkg/git/clone.go:102
|
||||
#: pkg/git/clone.go:63 pkg/git/clone.go:100
|
||||
#, c-format
|
||||
msgid "git clone %s: cancelled due to interrupt"
|
||||
msgstr "git clone %s: cancelado por interrupción"
|
||||
@@ -3640,17 +3702,17 @@ msgstr "git clone %s: cancelado por interrupción"
|
||||
msgid "git clone: %s"
|
||||
msgstr ""
|
||||
|
||||
#: pkg/git/clone.go:89
|
||||
#: pkg/git/clone.go:87
|
||||
#, c-format
|
||||
msgid "git clone: %s already exists"
|
||||
msgstr "git clone: %s ya existe"
|
||||
|
||||
#: pkg/git/clone.go:59 pkg/git/clone.go:78 pkg/git/clone.go:87
|
||||
#: pkg/git/clone.go:58 pkg/git/clone.go:76 pkg/git/clone.go:85
|
||||
#, c-format
|
||||
msgid "git clone: %s cloned successfully"
|
||||
msgstr "git clone: %s clonado correctamente"
|
||||
|
||||
#: pkg/git/clone.go:68
|
||||
#: pkg/git/clone.go:67
|
||||
msgid "git clone: main branch failed, attempting master branch"
|
||||
msgstr "git clone: error en la rama principal; intentando la rama master"
|
||||
|
||||
@@ -3705,7 +3767,7 @@ msgstr "git-nombre"
|
||||
msgid "git.coopcloud.tech repo exists"
|
||||
msgstr "el repositorio git.coopcloud.tech existe"
|
||||
|
||||
#: pkg/recipe/git.go:384
|
||||
#: pkg/recipe/git.go:390
|
||||
#, c-format
|
||||
msgid "git: opening repository in %s"
|
||||
msgstr "git: abriendo repositorio en %s"
|
||||
@@ -4643,7 +4705,7 @@ msgstr "mostrar salida legible por máquina"
|
||||
msgid "proceed?"
|
||||
msgstr "¿procedemos?"
|
||||
|
||||
#: pkg/recipe/git.go:404
|
||||
#: pkg/recipe/git.go:410
|
||||
#, c-format
|
||||
msgid "processing %s for %s"
|
||||
msgstr "procesando %s para %s"
|
||||
@@ -4713,7 +4775,7 @@ msgstr ""
|
||||
msgid "re"
|
||||
msgstr ""
|
||||
|
||||
#: pkg/recipe/git.go:157
|
||||
#: pkg/recipe/git.go:158
|
||||
#, c-format
|
||||
msgid "read %s as tags for recipe %s"
|
||||
msgstr "leer %s como etiquetas para la receta %s"
|
||||
@@ -5459,17 +5521,17 @@ msgstr "omitiendo generación de %s (generate=false)"
|
||||
msgid "skipping secret (because it already exists) on %s: %s"
|
||||
msgstr "omitiendo secreto (porque ya existe) en %s: %s"
|
||||
|
||||
#: pkg/recipe/git.go:413
|
||||
#: pkg/recipe/git.go:419
|
||||
#, c-format
|
||||
msgid "skipping tag %s: checkout failed: %s"
|
||||
msgstr "omitiendo la etiqueta %s: error en el checkout: %s"
|
||||
|
||||
#: pkg/recipe/git.go:422
|
||||
#: pkg/recipe/git.go:428
|
||||
#, c-format
|
||||
msgid "skipping tag %s: invalid compose config: %s"
|
||||
msgstr "omitiendo la etiqueta %s: configuración de compose inválida: %s"
|
||||
|
||||
#: pkg/recipe/git.go:432
|
||||
#: pkg/recipe/git.go:438
|
||||
#, c-format
|
||||
msgid "skipping tag %s: invalid image reference in service %s: %s"
|
||||
msgstr "omitiendo la etiqueta %s: referencia de imagen inválida en el servicio %s: %s"
|
||||
@@ -5572,7 +5634,7 @@ msgstr "recortado %s a %s para el análisis"
|
||||
msgid "succeeded"
|
||||
msgstr "exitoso"
|
||||
|
||||
#: pkg/recipe/git.go:184
|
||||
#: pkg/recipe/git.go:185
|
||||
#, c-format
|
||||
msgid "successfully checked %s out to %s in %s"
|
||||
msgstr "se comprobó correctamente %s de %s en %s"
|
||||
@@ -5734,17 +5796,17 @@ msgstr ""
|
||||
msgid "un"
|
||||
msgstr ""
|
||||
|
||||
#: pkg/recipe/git.go:193
|
||||
#: pkg/recipe/git.go:194
|
||||
#, c-format
|
||||
msgid "unable to check git clean status in %s: %s"
|
||||
msgstr "no se pudo comprobar el estado de git clean en %s: %s"
|
||||
|
||||
#: pkg/recipe/git.go:262
|
||||
#: pkg/recipe/git.go:263
|
||||
#, c-format
|
||||
msgid "unable to check out default branch in %s: %s"
|
||||
msgstr "no se pudo cambiar a la rama predeterminada en %s: %s"
|
||||
|
||||
#: pkg/git/clone.go:100
|
||||
#: pkg/git/clone.go:98
|
||||
#, c-format
|
||||
msgid "unable to clean up git clone of %s: %s"
|
||||
msgstr "no se pudo limpiar la clonación de Git de %s: %s"
|
||||
@@ -5818,7 +5880,7 @@ msgstr "no se pudieron determinar las reglas de versionado de %s; listando todas
|
||||
msgid "unable to discover SSH remote for %s"
|
||||
msgstr "no se pudo detectar el remoto SSH para %s"
|
||||
|
||||
#: pkg/recipe/git.go:268
|
||||
#: pkg/recipe/git.go:274
|
||||
#, c-format
|
||||
msgid "unable to fetch tags in %s: %s"
|
||||
msgstr "no se pudieron obtener las etiquetas en %s: %s"
|
||||
@@ -5828,7 +5890,7 @@ msgstr "no se pudieron obtener las etiquetas en %s: %s"
|
||||
msgid "unable to get container matching %s: %s"
|
||||
msgstr "no se pudo obtener el contenedor que coincide con %s: %s"
|
||||
|
||||
#: pkg/recipe/git.go:280
|
||||
#: pkg/recipe/git.go:286
|
||||
#, c-format
|
||||
msgid "unable to git pull in %s: %s"
|
||||
msgstr "no se pudo hacer git pull en %s: %s"
|
||||
@@ -5858,12 +5920,12 @@ msgid "unable to look up server context for %s: %s"
|
||||
msgstr "no se pudo obtener el contexto del servidor para %s: %s"
|
||||
|
||||
#: cli/recipe/fetch.go:77 pkg/git/read.go:26 pkg/lint/recipe.go:491
|
||||
#: pkg/recipe/git.go:242
|
||||
#: pkg/recipe/git.go:243
|
||||
#, c-format
|
||||
msgid "unable to open %s: %s"
|
||||
msgstr "no se pudo abrir %s: %s"
|
||||
|
||||
#: pkg/recipe/git.go:257
|
||||
#: pkg/recipe/git.go:258
|
||||
#, c-format
|
||||
msgid "unable to open git work tree in %s: %s"
|
||||
msgstr "no se pudo abrir el git work tree en %s: %s"
|
||||
@@ -5923,7 +5985,7 @@ msgstr "no se pudo consultar el estado de %s: %s"
|
||||
msgid "unable to read new env %s: %s"
|
||||
msgstr "no se pudo leer el nuevo env %s: %s"
|
||||
|
||||
#: pkg/recipe/git.go:247
|
||||
#: pkg/recipe/git.go:248
|
||||
#, c-format
|
||||
msgid "unable to read remotes in %s: %s"
|
||||
msgstr "no se pudieron leer los remotos en %s: %s"
|
||||
@@ -5959,7 +6021,7 @@ msgstr "no se pudo renderizar a JSON: %s"
|
||||
msgid "unable to reset commit after failed release attempt: %s"
|
||||
msgstr "no se pudo establecer los streams de IO como terminal raw: %s"
|
||||
|
||||
#: pkg/recipe/git.go:166
|
||||
#: pkg/recipe/git.go:167
|
||||
#, c-format
|
||||
msgid "unable to resolve '%s': %s"
|
||||
msgstr "no se pudo resolver '%s': %s"
|
||||
|
||||
+7
-1
@@ -16,6 +16,7 @@ import (
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/go-git/go-git/v5"
|
||||
gitCfg "github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
@@ -262,7 +263,12 @@ func (r Recipe) EnsureUpToDate() error {
|
||||
return errors.New(i18n.G("unable to check out default branch in %s: %s", r.Dir, err))
|
||||
}
|
||||
|
||||
fetchOpts := &git.FetchOptions{Tags: git.AllTags}
|
||||
// the refspec is passed explicitly, because a repository cloned by an older abra has a
|
||||
// single-branch refspec stored in its config and would otherwise never see other branches
|
||||
fetchOpts := &git.FetchOptions{
|
||||
Tags: git.AllTags,
|
||||
RefSpecs: []gitCfg.RefSpec{"+refs/heads/*:refs/remotes/origin/*"},
|
||||
}
|
||||
if err := repo.Fetch(fetchOpts); err != nil {
|
||||
if !strings.Contains(err.Error(), "already up-to-date") {
|
||||
return errors.New(i18n.G("unable to fetch tags in %s: %s", r.Dir, err))
|
||||
|
||||
@@ -3,7 +3,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: nginx:1.31.2
|
||||
image: nginx:1.31.5
|
||||
secrets:
|
||||
- test_pass_one
|
||||
- test_pass_two
|
||||
|
||||
@@ -122,6 +122,39 @@ teardown(){
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "create new app with commit from another branch" {
|
||||
branchHash=$(_get_other_branch_hash)
|
||||
if [[ -z "$branchHash" ]]; then
|
||||
skip "$TEST_RECIPE has no branch besides main"
|
||||
fi
|
||||
|
||||
# re-clone the recipe the way an older abra did, with a single-branch refspec. --no-local
|
||||
# forces a real transfer, a local clone would hardlink the whole object database and leave
|
||||
# the commit reachable
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE"
|
||||
assert_success
|
||||
|
||||
run git clone -q --no-local --single-branch --branch main \
|
||||
"$ABRA_DIR/origin-recipes/$TEST_RECIPE.git" "$ABRA_DIR/recipes/$TEST_RECIPE"
|
||||
assert_success
|
||||
|
||||
# the commit has to be genuinely missing, otherwise this test passes for the wrong reason
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --verify "$branchHash^{commit}"
|
||||
assert_failure
|
||||
|
||||
run $ABRA app new "$TEST_RECIPE" "$branchHash" \
|
||||
--no-input \
|
||||
--server "$TEST_SERVER" \
|
||||
--domain "$TEST_APP_DOMAIN"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
|
||||
# the recipe names itself in TYPE, which differs per branch, so only the version is checked
|
||||
run grep -q "TYPE=.*:${branchHash}$" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "does not overwrite existing env files" {
|
||||
run $ABRA app new "$TEST_RECIPE" \
|
||||
--no-input \
|
||||
|
||||
@@ -56,6 +56,13 @@ _get_tag_hash() {
|
||||
echo $(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-list -n 1 "$1")
|
||||
}
|
||||
|
||||
_get_other_branch_hash() {
|
||||
# asked from the origin mirror, not from the recipe checkout, so that the result does not
|
||||
# depend on what has been fetched. empty when the recipe only has a default branch
|
||||
echo $(git ls-remote "$ABRA_DIR/origin-recipes/$TEST_RECIPE.git" \
|
||||
| grep 'refs/heads/' | grep -v 'refs/heads/main$' | head -1 | cut -f1)
|
||||
}
|
||||
|
||||
_get_head_hash() {
|
||||
echo $(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show -s --format="%H" HEAD)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: nginx:1.31.2
|
||||
image: nginx:1.31.5
|
||||
networks:
|
||||
- proxy
|
||||
deploy:
|
||||
|
||||
+3
-3
@@ -3,16 +3,16 @@ kind: pipeline
|
||||
name: coopcloud.tech/tagcmp
|
||||
steps:
|
||||
- name: gofmt
|
||||
image: golang:1.26
|
||||
image: golang:1.27
|
||||
commands:
|
||||
- test -z "$(gofmt -l .)"
|
||||
|
||||
- name: go build
|
||||
image: golang:1.26
|
||||
image: golang:1.27
|
||||
commands:
|
||||
- go build -v .
|
||||
|
||||
- name: go test
|
||||
image: golang:1.26
|
||||
image: golang:1.27
|
||||
commands:
|
||||
- go test . -cover
|
||||
|
||||
+1
@@ -5,3 +5,4 @@ profile.out
|
||||
.tmp/
|
||||
.git-dist/
|
||||
.vscode
|
||||
build/tools/
|
||||
|
||||
+42
@@ -110,6 +110,48 @@ func (r ReferenceName) IsTag() bool {
|
||||
return strings.HasPrefix(string(r), refTagPrefix)
|
||||
}
|
||||
|
||||
// IsSafe reports whether the reference name can be safely turned into a path
|
||||
// under the .git directory, mirroring Git's refname_is_safe (refs.c). A name
|
||||
// is safe when it is either:
|
||||
//
|
||||
// - under "refs/", non-empty after the prefix, containing no backslash and
|
||||
// no empty, "." or ".." path component (so it cannot escape the refs/
|
||||
// sub-tree, or alias another name, once turned into a path); or
|
||||
// - a one-level pseudo-ref whose spelling is restricted to [A-Z_]
|
||||
// (e.g. HEAD, ORIG_HEAD, FETCH_HEAD).
|
||||
//
|
||||
// Everything else — a lowercase or mixed one-level name such as "config" or
|
||||
// "index", an absolute or drive-prefixed name, or a refs/ name that escapes —
|
||||
// is unsafe, because it could resolve onto unrelated repository metadata.
|
||||
func (r ReferenceName) IsSafe() bool {
|
||||
s := string(r)
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if rest, ok := strings.CutPrefix(s, refPrefix); ok {
|
||||
// '\' is a path separator on Windows, so a refs/ name containing one
|
||||
// could escape the sub-tree or alias another name once turned into a
|
||||
// path; reject it outright (check_refname_format forbids '\' too).
|
||||
if rest == "" || strings.Contains(rest, "\\") {
|
||||
return false
|
||||
}
|
||||
for part := range strings.SplitSeq(rest, "/") {
|
||||
if part == "" || part == "." || part == ".." {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
if (s[i] < 'A' || s[i] > 'Z') && s[i] != '_' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r ReferenceName) String() string {
|
||||
return string(r)
|
||||
}
|
||||
|
||||
+57
@@ -16,6 +16,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5/internal/pathutil"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/hash"
|
||||
"github.com/go-git/go-git/v5/storage"
|
||||
@@ -79,8 +80,52 @@ var (
|
||||
// resolve outside the modules/ subtree, mirroring canonical Git's
|
||||
// "ignoring suspicious submodule name" defence.
|
||||
ErrModuleNameEscape = errors.New("submodule name escapes modules/ directory")
|
||||
// ErrReferenceNameEscape is returned when a reference name would
|
||||
// resolve outside its reference sub-tree once turned into a path
|
||||
// under the .git directory (e.g. a name with a ".." component).
|
||||
ErrReferenceNameEscape = errors.New("reference name escapes the reference storage")
|
||||
)
|
||||
|
||||
// isPathSep reports whether r is a path separator in reference names.
|
||||
// It treats both '/' and '\\' as separators to harden against cross-OS paths.
|
||||
func isPathSep(r rune) bool { return r == '/' || r == '\\' }
|
||||
|
||||
// validReferenceName rejects reference names that cannot be safely turned into
|
||||
// a path under the .git directory. A loose reference is stored verbatim at
|
||||
// ".git/<name>", so a crafted name — for instance one advertised by a malicious
|
||||
// remote — could climb out of its reference sub-tree and read, overwrite, or
|
||||
// delete unrelated metadata such as .git/config.
|
||||
//
|
||||
// The storage-safety gate is plumbing.ReferenceName.IsSafe, mirroring Git's
|
||||
// refname_is_safe: a name must be under refs/ without escaping it, or be a
|
||||
// [A-Z_] pseudo-ref. This alone rejects absolute, drive-prefixed, escaping and
|
||||
// single-level metadata names. On top of it, this adds filesystem-specific
|
||||
// hardening that IsSafe's literal check does not cover: control characters, and
|
||||
// components a case-insensitive/NTFS/HFS+ filesystem would fold back to "." or
|
||||
// ".." (trailing dots/spaces, Alternate Data Streams, ignorable Unicode code
|
||||
// points), delegated to pathutil.IsHFSDot and pathutil.IsNTFSDot with "." as
|
||||
// the needle — as validSubmoduleName does — and run regardless of host OS.
|
||||
func validReferenceName(name plumbing.ReferenceName) error {
|
||||
if !name.IsSafe() {
|
||||
return fmt.Errorf("%w: %q is not under refs/ nor a valid pseudo-ref", ErrReferenceNameEscape, string(name))
|
||||
}
|
||||
|
||||
s := string(name)
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] < 0x20 || s[i] == 0x7f {
|
||||
return fmt.Errorf("%w: %q", ErrReferenceNameEscape, s)
|
||||
}
|
||||
}
|
||||
for _, part := range strings.FieldsFunc(s, isPathSep) {
|
||||
// IsNTFSDot/IsHFSDot with a "." needle match ".." and its disguises
|
||||
// but not a bare ".", so reject that component explicitly too.
|
||||
if part == "." || pathutil.IsHFSDot(part, ".") || pathutil.IsNTFSDot(part, ".", "") {
|
||||
return fmt.Errorf("%w: %q", ErrReferenceNameEscape, s)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Options holds configuration for the storage.
|
||||
type Options struct {
|
||||
// ExclusiveAccess means that the filesystem is not modified externally
|
||||
@@ -706,6 +751,10 @@ func (d *DotGit) checkReferenceAndTruncate(f billy.File, old *plumbing.Reference
|
||||
}
|
||||
|
||||
func (d *DotGit) SetRef(r, old *plumbing.Reference) error {
|
||||
if err := validReferenceName(r.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var content string
|
||||
switch r.Type() {
|
||||
case plumbing.SymbolicReference:
|
||||
@@ -741,6 +790,10 @@ func (d *DotGit) Refs() ([]*plumbing.Reference, error) {
|
||||
|
||||
// Ref returns the reference for a given reference name.
|
||||
func (d *DotGit) Ref(name plumbing.ReferenceName) (*plumbing.Reference, error) {
|
||||
if err := validReferenceName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ref, err := d.readReferenceFile(".", name.String())
|
||||
if err == nil {
|
||||
return ref, nil
|
||||
@@ -804,6 +857,10 @@ func (d *DotGit) packedRef(name plumbing.ReferenceName) (*plumbing.Reference, er
|
||||
|
||||
// RemoveRef removes a reference by name.
|
||||
func (d *DotGit) RemoveRef(name plumbing.ReferenceName) error {
|
||||
if err := validReferenceName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := d.fs.Join(".", name.String())
|
||||
_, err := d.fs.Stat(path)
|
||||
if err == nil {
|
||||
|
||||
+67
@@ -583,6 +583,10 @@ func (w *Worktree) checkoutChangeSubmodule(name string,
|
||||
return err
|
||||
}
|
||||
|
||||
if err := w.clearBlockingSymlinks(name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := w.Filesystem.MkdirAll(name, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -626,7 +630,70 @@ func (w *Worktree) checkoutChangeRegularFile(name string,
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearBlockingSymlinks removes a symlink that is in the way of
|
||||
// materialising name, so the checkout writes a real entry in its place
|
||||
// instead of following the link out of the worktree. Two cases:
|
||||
//
|
||||
// - a leading directory component that is a symlink (e.g. "s" while
|
||||
// writing "s/config", where "s" links to ".git"): OpenFile/MkdirAll
|
||||
// would traverse it, so the write would land under the link's target.
|
||||
// - the final component itself being a symlink (e.g. writing "s" while
|
||||
// "s" links to ".git/config"): OpenFile with O_TRUNC, or Symlink,
|
||||
// would follow/replace through it and clobber the target.
|
||||
//
|
||||
// A symlink can never be a legitimate parent of, or the destination for,
|
||||
// a tracked entry, so removing it is always correct. This mirrors upstream
|
||||
// Git's forced checkout, which unlinks a blocking symlink in the leading
|
||||
// path (create_directories) and unlinks an existing entry before
|
||||
// write_entry.
|
||||
// https://github.com/git/git/blob/v2.54.0/entry.c#L50
|
||||
func (w *Worktree) clearBlockingSymlinks(name string) error {
|
||||
var dirs []string
|
||||
for dir := filepath.Dir(name); dir != "." && dir != "" && dir != string(filepath.Separator); dir = filepath.Dir(dir) {
|
||||
dirs = append(dirs, dir)
|
||||
}
|
||||
// Leading components, shallowest-first: removing the shallowest symlink
|
||||
// invalidates every component beneath it, so a single removal is enough.
|
||||
for i := len(dirs) - 1; i >= 0; i-- {
|
||||
fi, err := w.Filesystem.Lstat(dirs[i])
|
||||
if err != nil {
|
||||
// A missing component is created as a real directory by the
|
||||
// checkout. Any other error means we cannot tell whether it is
|
||||
// a symlink, so surface it instead of leaving a blocking link in
|
||||
// place and failing later in a harder-to-diagnose way.
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
if fi.Mode()&os.ModeSymlink != 0 {
|
||||
return w.Filesystem.Remove(dirs[i])
|
||||
}
|
||||
}
|
||||
// Final component: an existing symlink here would be followed by the
|
||||
// subsequent OpenFile/Symlink/MkdirAll, so replace it.
|
||||
fi, err := w.Filesystem.Lstat(name)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if fi.Mode()&os.ModeSymlink != 0 {
|
||||
return w.Filesystem.Remove(name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Worktree) checkoutFile(f *object.File) (err error) {
|
||||
// checkoutFile is the materialisation boundary for tracked entries.
|
||||
// Remove any blocking symlink first so the subsequent OpenFile or
|
||||
// Symlink call writes the entry itself instead of following a planted
|
||||
// final-component link in the underlying filesystem.
|
||||
if err := w.clearBlockingSymlinks(f.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mode, err := f.Mode.ToOSFileMode()
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
+100
-15
@@ -35,10 +35,25 @@ func defaultProtectNTFS() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// worktreeFilesystem wraps a billy.Filesystem and validates every path passed
|
||||
// to a mutating operation. This prevents writing to, or deleting from,
|
||||
// dangerous locations (e.g. .git/*, ../) regardless of which worktree
|
||||
// code path triggers the operation.
|
||||
// worktreeFilesystem wraps a billy.Filesystem and validates every path it
|
||||
// is handed, so worktree operations cannot use dangerous paths at the
|
||||
// boundary. Two layers apply:
|
||||
//
|
||||
// - validPath rejects dangerous path *strings*: .git and its HFS+/NTFS
|
||||
// variants, "..", control characters, volume names.
|
||||
// - validNoLeadingSymlink rejects paths whose leading directories
|
||||
// already exist on disk as symlinks, so a write or delete cannot
|
||||
// follow a planted link out of the tree.
|
||||
//
|
||||
// Both layers run on every mutating operation (validWritePath) and every
|
||||
// read (validReadPath). Chroot additionally refuses a symlink as the final
|
||||
// component, so a sub-filesystem such as a submodule worktree cannot be
|
||||
// scoped to a redirected target.
|
||||
//
|
||||
// The wrapper intentionally stops at leading-component traversal. Callers
|
||||
// that need final-component no-follow semantics for materialisation
|
||||
// (checkoutFile) enforce that directly by removing the blocking symlink
|
||||
// before opening the destination path.
|
||||
type worktreeFilesystem struct {
|
||||
billy.Filesystem
|
||||
protectNTFS bool
|
||||
@@ -50,7 +65,7 @@ func newWorktreeFilesystem(fs billy.Filesystem, protectNTFS, protectHFS bool) *w
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Create(filename string) (billy.File, error) {
|
||||
if err := sfs.validPath(filename); err != nil {
|
||||
if err := sfs.validWritePath(filename); err != nil {
|
||||
return nil, fmt.Errorf("create: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Create(filename)
|
||||
@@ -64,7 +79,7 @@ func (sfs *worktreeFilesystem) Open(filename string) (billy.File, error) {
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
|
||||
if err := sfs.validPath(filename); err != nil {
|
||||
if err := sfs.validWritePath(filename); err != nil {
|
||||
return nil, fmt.Errorf("openfile: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.OpenFile(filename, flag, perm)
|
||||
@@ -78,14 +93,14 @@ func (sfs *worktreeFilesystem) Stat(filename string) (os.FileInfo, error) {
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Remove(filename string) error {
|
||||
if err := sfs.validPath(filename); err != nil {
|
||||
if err := sfs.validWritePath(filename); err != nil {
|
||||
return fmt.Errorf("remove: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Remove(filename)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Rename(from, to string) error {
|
||||
if err := sfs.validPath(from, to); err != nil {
|
||||
if err := sfs.validWritePath(from, to); err != nil {
|
||||
return fmt.Errorf("rename: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Rename(from, to)
|
||||
@@ -106,7 +121,7 @@ func (sfs *worktreeFilesystem) Lstat(filename string) (os.FileInfo, error) {
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Symlink(target, link string) error {
|
||||
if err := sfs.validPath(link); err != nil {
|
||||
if err := sfs.validWritePath(link); err != nil {
|
||||
return fmt.Errorf("symlink: %w", err)
|
||||
}
|
||||
if err := sfs.validSymlinkName(link); err != nil {
|
||||
@@ -131,7 +146,7 @@ func (sfs *worktreeFilesystem) MkdirAll(path string, perm os.FileMode) error {
|
||||
if path == "" || path == "." || path == "/" {
|
||||
return nil
|
||||
}
|
||||
if err := sfs.validPath(path); err != nil {
|
||||
if err := sfs.validWritePath(path); err != nil {
|
||||
return fmt.Errorf("mkdirall: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.MkdirAll(path, perm)
|
||||
@@ -145,18 +160,39 @@ func (sfs *worktreeFilesystem) Chroot(path string) (billy.Filesystem, error) {
|
||||
if err := sfs.validReadPath(path); err != nil {
|
||||
return nil, fmt.Errorf("chroot: %w", err)
|
||||
}
|
||||
// Chroot scopes a sub-filesystem to path, so the final component must
|
||||
// be a real directory too: a symlink there would silently redirect the
|
||||
// scope (e.g. a submodule worktree) to a target outside the tree. This
|
||||
// is the "valid path, wrong target" case that validNoLeadingSymlink,
|
||||
// which only inspects leading components, does not cover.
|
||||
//
|
||||
// A non-existent target is fine: Chroot creates it as a real
|
||||
// directory. Any other Lstat error means we cannot prove the target
|
||||
// is not a symlink, so fail closed rather than scope through it.
|
||||
if fi, err := sfs.Filesystem.Lstat(path); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("chroot: cannot stat %q: %w", path, err)
|
||||
}
|
||||
} else if fi.Mode()&os.ModeSymlink != 0 {
|
||||
return nil, fmt.Errorf("chroot: invalid path %q: is a symlink", path)
|
||||
}
|
||||
return sfs.Filesystem.Chroot(path)
|
||||
}
|
||||
|
||||
// validReadPath is like validPath but treats the empty string and "." as
|
||||
// valid references to the worktree root. Read-side operations on the root
|
||||
// (e.g. ReadDir(""), Lstat(".")) are legitimate; mutating the root itself
|
||||
// is not, so write-side operations continue to use validPath directly.
|
||||
// validReadPath is like validWritePath but treats the empty string and "."
|
||||
// as valid references to the worktree root. Read-side operations on the
|
||||
// root (e.g. ReadDir(""), Lstat(".")) are legitimate. Mutating the root
|
||||
// itself is not, so write-side operations reject it via validPath. Reads
|
||||
// are still refused through a leading symlink, so the wrapper never
|
||||
// follows a planted link even on the read surface.
|
||||
func (sfs *worktreeFilesystem) validReadPath(p string) error {
|
||||
if p == "" || p == "." || p == "/" {
|
||||
return nil
|
||||
}
|
||||
return sfs.validPath(p)
|
||||
if err := sfs.validPath(p); err != nil {
|
||||
return err
|
||||
}
|
||||
return sfs.validNoLeadingSymlink(p)
|
||||
}
|
||||
|
||||
var errUnsupportedOperation = errors.New("unsupported operation")
|
||||
@@ -232,6 +268,55 @@ func (sfs *worktreeFilesystem) validPath(paths ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validWritePath validates paths for mutating operations. It layers the
|
||||
// filesystem-state check validNoLeadingSymlink on top of the string-only
|
||||
// checks in validPath, so a write can neither name a dangerous path nor
|
||||
// reach one by traversing an existing symlink. Every mutating method on
|
||||
// the wrapper funnels through here, so the leading-symlink invariant holds
|
||||
// for all worktree writers without each call site having to remember it.
|
||||
func (sfs *worktreeFilesystem) validWritePath(paths ...string) error {
|
||||
if err := sfs.validPath(paths...); err != nil {
|
||||
return err
|
||||
}
|
||||
return sfs.validNoLeadingSymlink(paths...)
|
||||
}
|
||||
|
||||
// validNoLeadingSymlink rejects paths whose leading directory components
|
||||
// resolve through a symlink that already exists on the underlying
|
||||
// filesystem. validPath guards the path string. This guards the on-disk
|
||||
// state, so a write or delete cannot reach outside the worktree by
|
||||
// traversing a symlink that a tree or an earlier step left in place.
|
||||
//
|
||||
// This is the fail-closed backstop for the whole class. Callers that want
|
||||
// upstream's replace-and-continue behaviour (checkout) remove the blocking
|
||||
// symlink first via clearBlockingSymlinks, so no symlink remains when the
|
||||
// write reaches the wrapper. Callers that do not get a safe error,
|
||||
// matching upstream Git refusing rather than following the link. See
|
||||
// has_symlink_leading_path (symlinks.c) and the check_leading_path guard
|
||||
// in unlink_entry (entry.c).
|
||||
func (sfs *worktreeFilesystem) validNoLeadingSymlink(paths ...string) error {
|
||||
for _, p := range paths {
|
||||
for dir := filepath.Dir(p); dir != "." && dir != "" && dir != string(filepath.Separator); dir = filepath.Dir(dir) {
|
||||
fi, err := sfs.Filesystem.Lstat(dir)
|
||||
if err != nil {
|
||||
// A missing ancestor is materialised as a real directory,
|
||||
// so it cannot be a symlink and is safe to skip. Any other
|
||||
// error (permission, I/O) means we cannot prove the
|
||||
// component is not a symlink, so fail closed rather than
|
||||
// let the operation traverse an unverified component.
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("invalid path %q: cannot stat leading component %q: %w", p, dir, err)
|
||||
}
|
||||
if fi.Mode()&os.ModeSymlink != 0 {
|
||||
return fmt.Errorf("invalid path %q: leading component %q is a symlink", p, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validSymlinkName checks the per-component name of a symlink for
|
||||
// dotfile names that attackers can use to trick a checkout into
|
||||
// writing a dangerous symlink. Each path component is compared
|
||||
|
||||
+1
-1
@@ -371,7 +371,7 @@ func (w *Worktree) doAdd(path string, ignorePattern []gitignore.Pattern, skipSta
|
||||
}
|
||||
}
|
||||
|
||||
path = filepath.Clean(path)
|
||||
path = filepath.ToSlash(filepath.Clean(path))
|
||||
|
||||
if err != nil || !fi.IsDir() {
|
||||
added, h, err = w.doAddFile(idx, s, path, ignorePattern)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.26@sha256:f96cc555eb8db430159a3aa6797cd5bae561945b7b0fe7d0e284c63a3b291609
|
||||
FROM golang:1.27@sha256:512690a5660563b57d37ecc31129e7f136e831db2aed24a1dbeb8ad7380dc0fa
|
||||
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=arm
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.26@sha256:f96cc555eb8db430159a3aa6797cd5bae561945b7b0fe7d0e284c63a3b291609
|
||||
FROM golang:1.27@sha256:512690a5660563b57d37ecc31129e7f136e831db2aed24a1dbeb8ad7380dc0fa
|
||||
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=arm64
|
||||
|
||||
+20
-8
@@ -84,7 +84,7 @@ func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, ar
|
||||
return Equal(t, expected, actual, append([]interface{}{msg}, args...)...)
|
||||
}
|
||||
|
||||
// EqualErrorf asserts that a function returned an error (i.e. not `nil`)
|
||||
// EqualErrorf asserts that a function returned a non-nil error (i.e. an error)
|
||||
// and that it is equal to the provided error.
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
@@ -124,7 +124,7 @@ func EqualValuesf(t TestingT, expected interface{}, actual interface{}, msg stri
|
||||
return EqualValues(t, expected, actual, append([]interface{}{msg}, args...)...)
|
||||
}
|
||||
|
||||
// Errorf asserts that a function returned an error (i.e. not `nil`).
|
||||
// Errorf asserts that a function returned a non-nil error (ie. an error).
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// assert.Errorf(t, err, "error message %s", "formatted")
|
||||
@@ -144,8 +144,8 @@ func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...int
|
||||
return ErrorAs(t, err, target, append([]interface{}{msg}, args...)...)
|
||||
}
|
||||
|
||||
// ErrorContainsf asserts that a function returned an error (i.e. not `nil`)
|
||||
// and that the error contains the specified substring.
|
||||
// ErrorContainsf asserts that a function returned a non-nil error (i.e. an
|
||||
// error) and that the error contains the specified substring.
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// assert.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted")
|
||||
@@ -190,10 +190,10 @@ func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick
|
||||
// time.Sleep(8*time.Second)
|
||||
// externalValue = true
|
||||
// }()
|
||||
// assert.EventuallyWithTf(t, func(c *assert.CollectT, "error message %s", "formatted") {
|
||||
// assert.EventuallyWithTf(t, func(c *assert.CollectT) {
|
||||
// // add assertions as needed; any assertion failure will fail the current tick
|
||||
// assert.True(c, externalValue, "expected 'externalValue' to be true")
|
||||
// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false")
|
||||
// }, 10*time.Second, 1*time.Second, "error message %s", "formatted")
|
||||
func EventuallyWithTf(t TestingT, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool {
|
||||
if h, ok := t.(tHelper); ok {
|
||||
h.Helper()
|
||||
@@ -552,7 +552,7 @@ func NoDirExistsf(t TestingT, path string, msg string, args ...interface{}) bool
|
||||
return NoDirExists(t, path, append([]interface{}{msg}, args...)...)
|
||||
}
|
||||
|
||||
// NoErrorf asserts that a function returned no error (i.e. `nil`).
|
||||
// NoErrorf asserts that a function returned a nil error (ie. no error).
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// if assert.NoErrorf(t, err, "error message %s", "formatted") {
|
||||
@@ -849,7 +849,19 @@ func WithinRangef(t TestingT, actual time.Time, start time.Time, end time.Time,
|
||||
return WithinRange(t, actual, start, end, append([]interface{}{msg}, args...)...)
|
||||
}
|
||||
|
||||
// YAMLEqf asserts that two YAML strings are equivalent.
|
||||
// YAMLEqf asserts that the first documents in the two YAML strings are equivalent.
|
||||
//
|
||||
// expected := `---
|
||||
// key: value
|
||||
// ---
|
||||
// key: this is a second document, it is not evaluated
|
||||
// `
|
||||
// actual := `---
|
||||
// key: value
|
||||
// ---
|
||||
// key: this is a subsequent document, it is not evaluated
|
||||
// `
|
||||
// assert.YAMLEqf(t, expected, actual, "error message %s", "formatted")
|
||||
func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool {
|
||||
if h, ok := t.(tHelper); ok {
|
||||
h.Helper()
|
||||
|
||||
+38
-14
@@ -146,7 +146,7 @@ func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs
|
||||
return Equal(a.t, expected, actual, msgAndArgs...)
|
||||
}
|
||||
|
||||
// EqualError asserts that a function returned an error (i.e. not `nil`)
|
||||
// EqualError asserts that a function returned a non-nil error (i.e. an error)
|
||||
// and that it is equal to the provided error.
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
@@ -158,7 +158,7 @@ func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ...
|
||||
return EqualError(a.t, theError, errString, msgAndArgs...)
|
||||
}
|
||||
|
||||
// EqualErrorf asserts that a function returned an error (i.e. not `nil`)
|
||||
// EqualErrorf asserts that a function returned a non-nil error (i.e. an error)
|
||||
// and that it is equal to the provided error.
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
@@ -240,7 +240,7 @@ func (a *Assertions) Equalf(expected interface{}, actual interface{}, msg string
|
||||
return Equalf(a.t, expected, actual, msg, args...)
|
||||
}
|
||||
|
||||
// Error asserts that a function returned an error (i.e. not `nil`).
|
||||
// Error asserts that a function returned a non-nil error (ie. an error).
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// a.Error(err)
|
||||
@@ -269,8 +269,8 @@ func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args ..
|
||||
return ErrorAsf(a.t, err, target, msg, args...)
|
||||
}
|
||||
|
||||
// ErrorContains asserts that a function returned an error (i.e. not `nil`)
|
||||
// and that the error contains the specified substring.
|
||||
// ErrorContains asserts that a function returned a non-nil error (i.e. an
|
||||
// error) and that the error contains the specified substring.
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// a.ErrorContains(err, expectedErrorSubString)
|
||||
@@ -281,8 +281,8 @@ func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs .
|
||||
return ErrorContains(a.t, theError, contains, msgAndArgs...)
|
||||
}
|
||||
|
||||
// ErrorContainsf asserts that a function returned an error (i.e. not `nil`)
|
||||
// and that the error contains the specified substring.
|
||||
// ErrorContainsf asserts that a function returned a non-nil error (i.e. an
|
||||
// error) and that the error contains the specified substring.
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// a.ErrorContainsf(err, expectedErrorSubString, "error message %s", "formatted")
|
||||
@@ -311,7 +311,7 @@ func (a *Assertions) ErrorIsf(err error, target error, msg string, args ...inter
|
||||
return ErrorIsf(a.t, err, target, msg, args...)
|
||||
}
|
||||
|
||||
// Errorf asserts that a function returned an error (i.e. not `nil`).
|
||||
// Errorf asserts that a function returned a non-nil error (ie. an error).
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// a.Errorf(err, "error message %s", "formatted")
|
||||
@@ -372,10 +372,10 @@ func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor
|
||||
// time.Sleep(8*time.Second)
|
||||
// externalValue = true
|
||||
// }()
|
||||
// a.EventuallyWithTf(func(c *assert.CollectT, "error message %s", "formatted") {
|
||||
// a.EventuallyWithTf(func(c *assert.CollectT) {
|
||||
// // add assertions as needed; any assertion failure will fail the current tick
|
||||
// assert.True(c, externalValue, "expected 'externalValue' to be true")
|
||||
// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false")
|
||||
// }, 10*time.Second, 1*time.Second, "error message %s", "formatted")
|
||||
func (a *Assertions) EventuallyWithTf(condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool {
|
||||
if h, ok := a.t.(tHelper); ok {
|
||||
h.Helper()
|
||||
@@ -1096,7 +1096,7 @@ func (a *Assertions) NoDirExistsf(path string, msg string, args ...interface{})
|
||||
return NoDirExistsf(a.t, path, msg, args...)
|
||||
}
|
||||
|
||||
// NoError asserts that a function returned no error (i.e. `nil`).
|
||||
// NoError asserts that a function returned a nil error (ie. no error).
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// if a.NoError(err) {
|
||||
@@ -1109,7 +1109,7 @@ func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) bool {
|
||||
return NoError(a.t, err, msgAndArgs...)
|
||||
}
|
||||
|
||||
// NoErrorf asserts that a function returned no error (i.e. `nil`).
|
||||
// NoErrorf asserts that a function returned a nil error (ie. no error).
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// if a.NoErrorf(err, "error message %s", "formatted") {
|
||||
@@ -1690,7 +1690,19 @@ func (a *Assertions) WithinRangef(actual time.Time, start time.Time, end time.Ti
|
||||
return WithinRangef(a.t, actual, start, end, msg, args...)
|
||||
}
|
||||
|
||||
// YAMLEq asserts that two YAML strings are equivalent.
|
||||
// YAMLEq asserts that the first documents in the two YAML strings are equivalent.
|
||||
//
|
||||
// expected := `---
|
||||
// key: value
|
||||
// ---
|
||||
// key: this is a second document, it is not evaluated
|
||||
// `
|
||||
// actual := `---
|
||||
// key: value
|
||||
// ---
|
||||
// key: this is a subsequent document, it is not evaluated
|
||||
// `
|
||||
// a.YAMLEq(expected, actual)
|
||||
func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) bool {
|
||||
if h, ok := a.t.(tHelper); ok {
|
||||
h.Helper()
|
||||
@@ -1698,7 +1710,19 @@ func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interf
|
||||
return YAMLEq(a.t, expected, actual, msgAndArgs...)
|
||||
}
|
||||
|
||||
// YAMLEqf asserts that two YAML strings are equivalent.
|
||||
// YAMLEqf asserts that the first documents in the two YAML strings are equivalent.
|
||||
//
|
||||
// expected := `---
|
||||
// key: value
|
||||
// ---
|
||||
// key: this is a second document, it is not evaluated
|
||||
// `
|
||||
// actual := `---
|
||||
// key: value
|
||||
// ---
|
||||
// key: this is a subsequent document, it is not evaluated
|
||||
// `
|
||||
// a.YAMLEqf(expected, actual, "error message %s", "formatted")
|
||||
func (a *Assertions) YAMLEqf(expected string, actual string, msg string, args ...interface{}) bool {
|
||||
if h, ok := a.t.(tHelper); ok {
|
||||
h.Helper()
|
||||
|
||||
+13
-1
@@ -9,7 +9,7 @@ import (
|
||||
func isOrdered(t TestingT, object interface{}, allowedComparesResults []compareResult, failMessage string, msgAndArgs ...interface{}) bool {
|
||||
objKind := reflect.TypeOf(object).Kind()
|
||||
if objKind != reflect.Slice && objKind != reflect.Array {
|
||||
return false
|
||||
return Fail(t, fmt.Sprintf("object %T is not an ordered collection", object), msgAndArgs...)
|
||||
}
|
||||
|
||||
objValue := reflect.ValueOf(object)
|
||||
@@ -50,6 +50,9 @@ func isOrdered(t TestingT, object interface{}, allowedComparesResults []compareR
|
||||
// assert.IsIncreasing(t, []float{1, 2})
|
||||
// assert.IsIncreasing(t, []string{"a", "b"})
|
||||
func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
|
||||
if h, ok := t.(tHelper); ok {
|
||||
h.Helper()
|
||||
}
|
||||
return isOrdered(t, object, []compareResult{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...)
|
||||
}
|
||||
|
||||
@@ -59,6 +62,9 @@ func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo
|
||||
// assert.IsNonIncreasing(t, []float{2, 1})
|
||||
// assert.IsNonIncreasing(t, []string{"b", "a"})
|
||||
func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
|
||||
if h, ok := t.(tHelper); ok {
|
||||
h.Helper()
|
||||
}
|
||||
return isOrdered(t, object, []compareResult{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...)
|
||||
}
|
||||
|
||||
@@ -68,6 +74,9 @@ func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{})
|
||||
// assert.IsDecreasing(t, []float{2, 1})
|
||||
// assert.IsDecreasing(t, []string{"b", "a"})
|
||||
func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
|
||||
if h, ok := t.(tHelper); ok {
|
||||
h.Helper()
|
||||
}
|
||||
return isOrdered(t, object, []compareResult{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...)
|
||||
}
|
||||
|
||||
@@ -77,5 +86,8 @@ func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo
|
||||
// assert.IsNonDecreasing(t, []float{1, 2})
|
||||
// assert.IsNonDecreasing(t, []string{"a", "b"})
|
||||
func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
|
||||
if h, ok := t.(tHelper); ok {
|
||||
h.Helper()
|
||||
}
|
||||
return isOrdered(t, object, []compareResult{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...)
|
||||
}
|
||||
|
||||
+83
-64
@@ -17,11 +17,10 @@ import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/pmezard/go-difflib/difflib"
|
||||
|
||||
// Wrapper around gopkg.in/yaml.v3
|
||||
// Wrapper around go.yaml.in/yaml/v3
|
||||
"github.com/stretchr/testify/assert/yaml"
|
||||
"github.com/stretchr/testify/internal/difflib"
|
||||
"github.com/stretchr/testify/internal/spew"
|
||||
)
|
||||
|
||||
//go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=assert -template=assertion_format.go.tmpl"
|
||||
@@ -33,19 +32,19 @@ type TestingT interface {
|
||||
|
||||
// ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful
|
||||
// for table driven tests.
|
||||
type ComparisonAssertionFunc func(TestingT, interface{}, interface{}, ...interface{}) bool
|
||||
type ComparisonAssertionFunc = func(TestingT, interface{}, interface{}, ...interface{}) bool
|
||||
|
||||
// ValueAssertionFunc is a common function prototype when validating a single value. Can be useful
|
||||
// for table driven tests.
|
||||
type ValueAssertionFunc func(TestingT, interface{}, ...interface{}) bool
|
||||
type ValueAssertionFunc = func(TestingT, interface{}, ...interface{}) bool
|
||||
|
||||
// BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful
|
||||
// for table driven tests.
|
||||
type BoolAssertionFunc func(TestingT, bool, ...interface{}) bool
|
||||
type BoolAssertionFunc = func(TestingT, bool, ...interface{}) bool
|
||||
|
||||
// ErrorAssertionFunc is a common function prototype when validating an error value. Can be useful
|
||||
// for table driven tests.
|
||||
type ErrorAssertionFunc func(TestingT, error, ...interface{}) bool
|
||||
type ErrorAssertionFunc = func(TestingT, error, ...interface{}) bool
|
||||
|
||||
// PanicAssertionFunc is a common function prototype when validating a panic value. Can be useful
|
||||
// for table driven tests.
|
||||
@@ -325,13 +324,15 @@ func messageFromMsgAndArgs(msgAndArgs ...interface{}) string {
|
||||
func indentMessageLines(message string, longestLabelLen int) string {
|
||||
outBuf := new(bytes.Buffer)
|
||||
|
||||
for i, scanner := 0, bufio.NewScanner(strings.NewReader(message)); scanner.Scan(); i++ {
|
||||
// no need to align first line because it starts at the correct location (after the label)
|
||||
if i != 0 {
|
||||
// append alignLen+1 spaces to align with "{{longestLabel}}:" before adding tab
|
||||
outBuf.WriteString("\n\t" + strings.Repeat(" ", longestLabelLen+1) + "\t")
|
||||
scanner := bufio.NewScanner(strings.NewReader(message))
|
||||
for firstLine := true; scanner.Scan(); firstLine = false {
|
||||
if !firstLine {
|
||||
fmt.Fprint(outBuf, "\n\t"+strings.Repeat(" ", longestLabelLen+1)+"\t")
|
||||
}
|
||||
outBuf.WriteString(scanner.Text())
|
||||
fmt.Fprint(outBuf, scanner.Text())
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Sprintf("cannot display message: %s", err)
|
||||
}
|
||||
|
||||
return outBuf.String()
|
||||
@@ -544,9 +545,8 @@ func Same(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) b
|
||||
if !same {
|
||||
// both are pointers but not the same type & pointing to the same address
|
||||
return Fail(t, fmt.Sprintf("Not same: \n"+
|
||||
"expected: %p %#[1]v\n"+
|
||||
"actual : %p %#[2]v",
|
||||
expected, actual), msgAndArgs...)
|
||||
"expected: %[2]s (%[1]T)(%[1]p)\n"+
|
||||
"actual : %[4]s (%[3]T)(%[3]p)", expected, truncatingFormat("%#v", expected), actual, truncatingFormat("%#v", actual)), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -571,8 +571,8 @@ func NotSame(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}
|
||||
|
||||
if same {
|
||||
return Fail(t, fmt.Sprintf(
|
||||
"Expected and actual point to the same object: %p %#[1]v",
|
||||
expected), msgAndArgs...)
|
||||
"Expected and actual point to the same object: %p %s",
|
||||
expected, truncatingFormat("%#v", expected)), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -604,25 +604,26 @@ func samePointers(first, second interface{}) (same bool, ok bool) {
|
||||
// to a type conversion in the Go grammar.
|
||||
func formatUnequalValues(expected, actual interface{}) (e string, a string) {
|
||||
if reflect.TypeOf(expected) != reflect.TypeOf(actual) {
|
||||
return fmt.Sprintf("%T(%s)", expected, truncatingFormat(expected)),
|
||||
fmt.Sprintf("%T(%s)", actual, truncatingFormat(actual))
|
||||
return fmt.Sprintf("%T(%s)", expected, truncatingFormat("%#v", expected)),
|
||||
fmt.Sprintf("%T(%s)", actual, truncatingFormat("%#v", actual))
|
||||
}
|
||||
switch expected.(type) {
|
||||
case time.Duration:
|
||||
return fmt.Sprintf("%v", expected), fmt.Sprintf("%v", actual)
|
||||
}
|
||||
return truncatingFormat(expected), truncatingFormat(actual)
|
||||
return truncatingFormat("%#v", expected), truncatingFormat("%#v", actual)
|
||||
}
|
||||
|
||||
// truncatingFormat formats the data and truncates it if it's too long.
|
||||
//
|
||||
// This helps keep formatted error messages lines from exceeding the
|
||||
// bufio.MaxScanTokenSize max line length that the go testing framework imposes.
|
||||
func truncatingFormat(data interface{}) string {
|
||||
value := fmt.Sprintf("%#v", data)
|
||||
max := bufio.MaxScanTokenSize - 100 // Give us some space the type info too if needed.
|
||||
if len(value) > max {
|
||||
value = value[0:max] + "<... truncated>"
|
||||
func truncatingFormat(format string, data interface{}) string {
|
||||
value := fmt.Sprintf(format, data)
|
||||
// Give us space for two truncated objects and the surrounding sentence.
|
||||
maxMessageSize := bufio.MaxScanTokenSize/2 - 100
|
||||
if len(value) > maxMessageSize {
|
||||
value = value[0:maxMessageSize] + "<... truncated>"
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -743,7 +744,7 @@ func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
|
||||
if h, ok := t.(tHelper); ok {
|
||||
h.Helper()
|
||||
}
|
||||
return Fail(t, fmt.Sprintf("Expected nil, but got: %#v", object), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("Expected nil, but got: %s", truncatingFormat("%#v", object)), msgAndArgs...)
|
||||
}
|
||||
|
||||
// isEmpty gets whether the specified object is considered empty or not.
|
||||
@@ -793,7 +794,7 @@ func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
|
||||
if h, ok := t.(tHelper); ok {
|
||||
h.Helper()
|
||||
}
|
||||
Fail(t, fmt.Sprintf("Should be empty, but was %v", object), msgAndArgs...)
|
||||
Fail(t, fmt.Sprintf("Should be empty, but was %s", truncatingFormat("%v", object)), msgAndArgs...)
|
||||
}
|
||||
|
||||
return pass
|
||||
@@ -836,11 +837,11 @@ func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{})
|
||||
}
|
||||
l, ok := getLen(object)
|
||||
if !ok {
|
||||
return Fail(t, fmt.Sprintf("\"%v\" could not be applied builtin len()", object), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("%q could not be applied builtin len()", truncatingFormat("%v", object)), msgAndArgs...)
|
||||
}
|
||||
|
||||
if l != length {
|
||||
return Fail(t, fmt.Sprintf("\"%v\" should have %d item(s), but has %d", object, length, l), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("%q should have %d item(s), but has %d", truncatingFormat("%v", object), length, l), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -889,7 +890,7 @@ func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{
|
||||
}
|
||||
|
||||
if ObjectsAreEqual(expected, actual) {
|
||||
return Fail(t, fmt.Sprintf("Should not be: %#v\n", actual), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("Should not be: %s\n", truncatingFormat("%#v", actual)), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -904,7 +905,7 @@ func NotEqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...inte
|
||||
}
|
||||
|
||||
if ObjectsAreEqualValues(expected, actual) {
|
||||
return Fail(t, fmt.Sprintf("Should not be: %#v\n", actual), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("Should not be: %s\n", truncatingFormat("%#v", actual)), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -964,10 +965,10 @@ func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bo
|
||||
|
||||
ok, found := containsElement(s, contains)
|
||||
if !ok {
|
||||
return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", s), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("%s could not be applied builtin len()", truncatingFormat("%#v", s)), msgAndArgs...)
|
||||
}
|
||||
if !found {
|
||||
return Fail(t, fmt.Sprintf("%#v does not contain %#v", s, contains), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("%s does not contain %#v", truncatingFormat("%#v", s), contains), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -986,10 +987,10 @@ func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{})
|
||||
|
||||
ok, found := containsElement(s, contains)
|
||||
if !ok {
|
||||
return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", s), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("%s could not be applied builtin len()", truncatingFormat("%#v", s)), msgAndArgs...)
|
||||
}
|
||||
if found {
|
||||
return Fail(t, fmt.Sprintf("%#v should not contain %#v", s, contains), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("%s should not contain %#v", truncatingFormat("%#v", s), contains), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -1031,10 +1032,10 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok
|
||||
av := actualMap.MapIndex(k)
|
||||
|
||||
if !av.IsValid() {
|
||||
return Fail(t, fmt.Sprintf("%#v does not contain %#v", list, subset), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("%s does not contain %s", truncatingFormat("%#v", list), truncatingFormat("%#v", subset)), msgAndArgs...)
|
||||
}
|
||||
if !ObjectsAreEqual(ev.Interface(), av.Interface()) {
|
||||
return Fail(t, fmt.Sprintf("%#v does not contain %#v", list, subset), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("%s does not contain %s", truncatingFormat("%#v", list), truncatingFormat("%#v", subset)), msgAndArgs...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1056,7 +1057,7 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok
|
||||
return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", list), msgAndArgs...)
|
||||
}
|
||||
if !found {
|
||||
return Fail(t, fmt.Sprintf("%#v does not contain %#v", list, element), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("%s does not contain %#v", truncatingFormat("%#v", list), element), msgAndArgs...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1082,12 +1083,12 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{})
|
||||
|
||||
listKind := reflect.TypeOf(list).Kind()
|
||||
if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map {
|
||||
return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("%#v has an unsupported type %s", list, listKind), msgAndArgs...)
|
||||
}
|
||||
|
||||
subsetKind := reflect.TypeOf(subset).Kind()
|
||||
if subsetKind != reflect.Array && subsetKind != reflect.Slice && subsetKind != reflect.Map {
|
||||
return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("%#v has an unsupported type %s", subset, subsetKind), msgAndArgs...)
|
||||
}
|
||||
|
||||
if subsetKind == reflect.Map && listKind == reflect.Map {
|
||||
@@ -1106,7 +1107,7 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{})
|
||||
}
|
||||
}
|
||||
|
||||
return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("%s is a subset of %s", truncatingFormat("%#v", subset), truncatingFormat("%#v", list)), msgAndArgs...)
|
||||
}
|
||||
|
||||
subsetList := reflect.ValueOf(subset)
|
||||
@@ -1121,14 +1122,14 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{})
|
||||
element := subsetList.Index(i).Interface()
|
||||
ok, found := containsElement(list, element)
|
||||
if !ok {
|
||||
return Fail(t, fmt.Sprintf("%q could not be applied builtin len()", list), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", list), msgAndArgs...)
|
||||
}
|
||||
if !found {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("%s is a subset of %s", truncatingFormat("%#v", subset), truncatingFormat("%#v", list)), msgAndArgs...)
|
||||
}
|
||||
|
||||
// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified
|
||||
@@ -1343,9 +1344,15 @@ func PanicsWithError(t TestingT, errString string, f PanicTestFunc, msgAndArgs .
|
||||
if !funcDidPanic {
|
||||
return Fail(t, fmt.Sprintf("func %#v should panic\n\tPanic value:\t%#v", f, panicValue), msgAndArgs...)
|
||||
}
|
||||
panicErr, ok := panicValue.(error)
|
||||
if !ok || panicErr.Error() != errString {
|
||||
return Fail(t, fmt.Sprintf("func %#v should panic with error message:\t%#v\n\tPanic value:\t%#v\n\tPanic stack:\t%s", f, errString, panicValue, panickedStack), msgAndArgs...)
|
||||
panicErr, isError := panicValue.(error)
|
||||
if !isError || panicErr.Error() != errString {
|
||||
msg := fmt.Sprintf("func %#v should panic with error message:\t%#v\n", f, errString)
|
||||
if isError {
|
||||
msg += fmt.Sprintf("\tError message:\t%#v\n", panicErr.Error())
|
||||
}
|
||||
msg += fmt.Sprintf("\tPanic value:\t%#v\n", panicValue)
|
||||
msg += fmt.Sprintf("\tPanic stack:\t%s\n", panickedStack)
|
||||
return Fail(t, msg, msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -1624,7 +1631,7 @@ func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, m
|
||||
Errors
|
||||
*/
|
||||
|
||||
// NoError asserts that a function returned no error (i.e. `nil`).
|
||||
// NoError asserts that a function returned a nil error (ie. no error).
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// if assert.NoError(t, err) {
|
||||
@@ -1635,13 +1642,13 @@ func NoError(t TestingT, err error, msgAndArgs ...interface{}) bool {
|
||||
if h, ok := t.(tHelper); ok {
|
||||
h.Helper()
|
||||
}
|
||||
return Fail(t, fmt.Sprintf("Received unexpected error:\n%+v", err), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("Received unexpected error:\n%s", truncatingFormat("%+v", err)), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Error asserts that a function returned an error (i.e. not `nil`).
|
||||
// Error asserts that a function returned a non-nil error (ie. an error).
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// assert.Error(t, err)
|
||||
@@ -1656,7 +1663,7 @@ func Error(t TestingT, err error, msgAndArgs ...interface{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// EqualError asserts that a function returned an error (i.e. not `nil`)
|
||||
// EqualError asserts that a function returned a non-nil error (i.e. an error)
|
||||
// and that it is equal to the provided error.
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
@@ -1674,13 +1681,13 @@ func EqualError(t TestingT, theError error, errString string, msgAndArgs ...inte
|
||||
if expected != actual {
|
||||
return Fail(t, fmt.Sprintf("Error message not equal:\n"+
|
||||
"expected: %q\n"+
|
||||
"actual : %q", expected, actual), msgAndArgs...)
|
||||
"actual : %s", expected, truncatingFormat("%q", actual)), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ErrorContains asserts that a function returned an error (i.e. not `nil`)
|
||||
// and that the error contains the specified substring.
|
||||
// ErrorContains asserts that a function returned a non-nil error (i.e. an
|
||||
// error) and that the error contains the specified substring.
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// assert.ErrorContains(t, err, expectedErrorSubString)
|
||||
@@ -1694,7 +1701,7 @@ func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...in
|
||||
|
||||
actual := theError.Error()
|
||||
if !strings.Contains(actual, contains) {
|
||||
return Fail(t, fmt.Sprintf("Error %#v does not contain %#v", actual, contains), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("Error %s does not contain %#v", truncatingFormat("%#v", actual), contains), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -1760,7 +1767,7 @@ func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool {
|
||||
h.Helper()
|
||||
}
|
||||
if i != nil && !reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface()) {
|
||||
return Fail(t, fmt.Sprintf("Should be zero, but was %v", i), msgAndArgs...)
|
||||
return Fail(t, fmt.Sprintf("Should be zero, but was %s", truncatingFormat("%v", i)), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1874,7 +1881,19 @@ func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{
|
||||
return Equal(t, expectedJSONAsInterface, actualJSONAsInterface, msgAndArgs...)
|
||||
}
|
||||
|
||||
// YAMLEq asserts that two YAML strings are equivalent.
|
||||
// YAMLEq asserts that the first documents in the two YAML strings are equivalent.
|
||||
//
|
||||
// expected := `---
|
||||
// key: value
|
||||
// ---
|
||||
// key: this is a second document, it is not evaluated
|
||||
// `
|
||||
// actual := `---
|
||||
// key: value
|
||||
// ---
|
||||
// key: this is a subsequent document, it is not evaluated
|
||||
// `
|
||||
// assert.YAMLEq(t, expected, actual)
|
||||
func YAMLEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool {
|
||||
if h, ok := t.(tHelper); ok {
|
||||
h.Helper()
|
||||
@@ -2188,8 +2207,8 @@ func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool {
|
||||
chain := buildErrorChainString(err, false)
|
||||
|
||||
return Fail(t, fmt.Sprintf("Target error should be in err chain:\n"+
|
||||
"expected: %q\n"+
|
||||
"in chain: %s", expectedText, chain,
|
||||
"expected: %s\n"+
|
||||
"in chain: %s", truncatingFormat("%q", expectedText), truncatingFormat("%s", chain),
|
||||
), msgAndArgs...)
|
||||
}
|
||||
|
||||
@@ -2211,8 +2230,8 @@ func NotErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool {
|
||||
chain := buildErrorChainString(err, false)
|
||||
|
||||
return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+
|
||||
"found: %q\n"+
|
||||
"in chain: %s", expectedText, chain,
|
||||
"found: %s\n"+
|
||||
"in chain: %s", truncatingFormat("%q", expectedText), truncatingFormat("%s", chain),
|
||||
), msgAndArgs...)
|
||||
}
|
||||
|
||||
@@ -2236,7 +2255,7 @@ func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{
|
||||
|
||||
return Fail(t, fmt.Sprintf("Should be in error chain:\n"+
|
||||
"expected: %s\n"+
|
||||
"in chain: %s", expectedType, chain,
|
||||
"in chain: %s", expectedType, truncatingFormat("%s", chain),
|
||||
), msgAndArgs...)
|
||||
}
|
||||
|
||||
@@ -2254,7 +2273,7 @@ func NotErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interfa
|
||||
|
||||
return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+
|
||||
"found: %s\n"+
|
||||
"in chain: %s", reflect.TypeOf(target).Elem().String(), chain,
|
||||
"in chain: %s", reflect.TypeOf(target).Elem().String(), truncatingFormat("%s", chain),
|
||||
), msgAndArgs...)
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -40,8 +40,8 @@
|
||||
//
|
||||
// # Assertions
|
||||
//
|
||||
// Assertions allow you to easily write test code, and are global funcs in the `assert` package.
|
||||
// All assertion functions take, as the first argument, the `*testing.T` object provided by the
|
||||
// Assertions allow you to easily write test code, and are global funcs in the assert package.
|
||||
// All assertion functions take, as the first argument, the [*testing.T] object provided by the
|
||||
// testing framework. This allows the assertion funcs to write the failings and other details to
|
||||
// the correct place.
|
||||
//
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
// go test -tags testify_yaml_custom
|
||||
//
|
||||
// This implementation can be used at build time to replace the default implementation
|
||||
// to avoid linking with [gopkg.in/yaml.v3].
|
||||
// to avoid linking with [go.yaml.in/yaml/v3].
|
||||
//
|
||||
// In your test package:
|
||||
//
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@
|
||||
// indirection with an alternative implementation of this package that uses
|
||||
// another implementation of YAML deserialization. This allows to not either not
|
||||
// use YAML deserialization at all, or to use another implementation than
|
||||
// [gopkg.in/yaml.v3] (for example for license compatibility reasons, see [PR #1120]).
|
||||
// [go.yaml.in/yaml/v3] (for example for license compatibility reasons, see [PR #1120]).
|
||||
//
|
||||
// Alternative implementations are selected using build tags:
|
||||
//
|
||||
@@ -28,9 +28,9 @@
|
||||
// [PR #1120]: https://github.com/stretchr/testify/pull/1120
|
||||
package yaml
|
||||
|
||||
import goyaml "gopkg.in/yaml.v3"
|
||||
import goyaml "go.yaml.in/yaml/v3"
|
||||
|
||||
// Unmarshal is just a wrapper of [gopkg.in/yaml.v3.Unmarshal].
|
||||
// Unmarshal is just a wrapper of [go.yaml.in/yaml/v3.Unmarshal].
|
||||
func Unmarshal(in []byte, out interface{}) error {
|
||||
return goyaml.Unmarshal(in, out)
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
// Package yaml is an implementation of YAML functions that always fail.
|
||||
//
|
||||
// This implementation can be used at build time to replace the default implementation
|
||||
// to avoid linking with [gopkg.in/yaml.v3]:
|
||||
// to avoid linking with [go.yaml.in/yaml/v3]:
|
||||
//
|
||||
// go test -tags testify_yaml_fail
|
||||
package yaml
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -24,4 +24,4 @@ TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Generated
Vendored
+13
-193
@@ -8,11 +8,14 @@
|
||||
//
|
||||
// - unified_diff
|
||||
//
|
||||
// - context_diff
|
||||
//
|
||||
// Getting unified diffs was the main goal of the port. Keep in mind this code
|
||||
// is mostly suitable to output text differences in a human friendly way, there
|
||||
// are no guarantees generated diffs are consumable by patch(1).
|
||||
//
|
||||
// This package was adopted from [github.com/pmezard/go-difflib] which
|
||||
// is no longer maintained.
|
||||
//
|
||||
// [github.com/pmezard/go-difflib]: https://github.com/pmezard/go-difflib
|
||||
package difflib
|
||||
|
||||
import (
|
||||
@@ -37,13 +40,6 @@ func max(a, b int) int {
|
||||
return b
|
||||
}
|
||||
|
||||
func calculateRatio(matches, length int) float64 {
|
||||
if length > 0 {
|
||||
return 2.0 * float64(matches) / float64(length)
|
||||
}
|
||||
return 1.0
|
||||
}
|
||||
|
||||
type Match struct {
|
||||
A int
|
||||
B int
|
||||
@@ -103,14 +99,6 @@ func NewMatcher(a, b []string) *SequenceMatcher {
|
||||
return &m
|
||||
}
|
||||
|
||||
func NewMatcherWithJunk(a, b []string, autoJunk bool,
|
||||
isJunk func(string) bool) *SequenceMatcher {
|
||||
|
||||
m := SequenceMatcher{IsJunk: isJunk, autoJunk: autoJunk}
|
||||
m.SetSeqs(a, b)
|
||||
return &m
|
||||
}
|
||||
|
||||
// Set two sequences to be compared.
|
||||
func (m *SequenceMatcher) SetSeqs(a, b []string) {
|
||||
m.SetSeq1(a)
|
||||
@@ -199,12 +187,15 @@ func (m *SequenceMatcher) isBJunk(s string) bool {
|
||||
// If IsJunk is not defined:
|
||||
//
|
||||
// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where
|
||||
// alo <= i <= i+k <= ahi
|
||||
// blo <= j <= j+k <= bhi
|
||||
//
|
||||
// alo <= i <= i+k <= ahi
|
||||
// blo <= j <= j+k <= bhi
|
||||
//
|
||||
// and for all (i',j',k') meeting those conditions,
|
||||
// k >= k'
|
||||
// i <= i'
|
||||
// and if i == i', j <= j'
|
||||
//
|
||||
// k >= k'
|
||||
// i <= i'
|
||||
// and if i == i', j <= j'
|
||||
//
|
||||
// In other words, of all maximal matching blocks, return one that
|
||||
// starts earliest in a, and of all those maximal matching blocks that
|
||||
@@ -451,66 +442,6 @@ func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode {
|
||||
return groups
|
||||
}
|
||||
|
||||
// Return a measure of the sequences' similarity (float in [0,1]).
|
||||
//
|
||||
// Where T is the total number of elements in both sequences, and
|
||||
// M is the number of matches, this is 2.0*M / T.
|
||||
// Note that this is 1 if the sequences are identical, and 0 if
|
||||
// they have nothing in common.
|
||||
//
|
||||
// .Ratio() is expensive to compute if you haven't already computed
|
||||
// .GetMatchingBlocks() or .GetOpCodes(), in which case you may
|
||||
// want to try .QuickRatio() or .RealQuickRation() first to get an
|
||||
// upper bound.
|
||||
func (m *SequenceMatcher) Ratio() float64 {
|
||||
matches := 0
|
||||
for _, m := range m.GetMatchingBlocks() {
|
||||
matches += m.Size
|
||||
}
|
||||
return calculateRatio(matches, len(m.a)+len(m.b))
|
||||
}
|
||||
|
||||
// Return an upper bound on ratio() relatively quickly.
|
||||
//
|
||||
// This isn't defined beyond that it is an upper bound on .Ratio(), and
|
||||
// is faster to compute.
|
||||
func (m *SequenceMatcher) QuickRatio() float64 {
|
||||
// viewing a and b as multisets, set matches to the cardinality
|
||||
// of their intersection; this counts the number of matches
|
||||
// without regard to order, so is clearly an upper bound
|
||||
if m.fullBCount == nil {
|
||||
m.fullBCount = map[string]int{}
|
||||
for _, s := range m.b {
|
||||
m.fullBCount[s] = m.fullBCount[s] + 1
|
||||
}
|
||||
}
|
||||
|
||||
// avail[x] is the number of times x appears in 'b' less the
|
||||
// number of times we've seen it in 'a' so far ... kinda
|
||||
avail := map[string]int{}
|
||||
matches := 0
|
||||
for _, s := range m.a {
|
||||
n, ok := avail[s]
|
||||
if !ok {
|
||||
n = m.fullBCount[s]
|
||||
}
|
||||
avail[s] = n - 1
|
||||
if n > 0 {
|
||||
matches += 1
|
||||
}
|
||||
}
|
||||
return calculateRatio(matches, len(m.a)+len(m.b))
|
||||
}
|
||||
|
||||
// Return an upper bound on ratio() very quickly.
|
||||
//
|
||||
// This isn't defined beyond that it is an upper bound on .Ratio(), and
|
||||
// is faster to compute than either .Ratio() or .QuickRatio().
|
||||
func (m *SequenceMatcher) RealQuickRatio() float64 {
|
||||
la, lb := len(m.a), len(m.b)
|
||||
return calculateRatio(min(la, lb), la+lb)
|
||||
}
|
||||
|
||||
// Convert range to the "ed" format
|
||||
func formatRangeUnified(start, stop int) string {
|
||||
// Per the diff spec at http://www.unix.org/single_unix_specification/
|
||||
@@ -652,117 +583,6 @@ func formatRangeContext(start, stop int) string {
|
||||
return fmt.Sprintf("%d,%d", beginning, beginning+length-1)
|
||||
}
|
||||
|
||||
type ContextDiff UnifiedDiff
|
||||
|
||||
// Compare two sequences of lines; generate the delta as a context diff.
|
||||
//
|
||||
// Context diffs are a compact way of showing line changes and a few
|
||||
// lines of context. The number of context lines is set by diff.Context
|
||||
// which defaults to three.
|
||||
//
|
||||
// By default, the diff control lines (those with *** or ---) are
|
||||
// created with a trailing newline.
|
||||
//
|
||||
// For inputs that do not have trailing newlines, set the diff.Eol
|
||||
// argument to "" so that the output will be uniformly newline free.
|
||||
//
|
||||
// The context diff format normally has a header for filenames and
|
||||
// modification times. Any or all of these may be specified using
|
||||
// strings for diff.FromFile, diff.ToFile, diff.FromDate, diff.ToDate.
|
||||
// The modification times are normally expressed in the ISO 8601 format.
|
||||
// If not specified, the strings default to blanks.
|
||||
func WriteContextDiff(writer io.Writer, diff ContextDiff) error {
|
||||
buf := bufio.NewWriter(writer)
|
||||
defer buf.Flush()
|
||||
var diffErr error
|
||||
wf := func(format string, args ...interface{}) {
|
||||
_, err := buf.WriteString(fmt.Sprintf(format, args...))
|
||||
if diffErr == nil && err != nil {
|
||||
diffErr = err
|
||||
}
|
||||
}
|
||||
ws := func(s string) {
|
||||
_, err := buf.WriteString(s)
|
||||
if diffErr == nil && err != nil {
|
||||
diffErr = err
|
||||
}
|
||||
}
|
||||
|
||||
if len(diff.Eol) == 0 {
|
||||
diff.Eol = "\n"
|
||||
}
|
||||
|
||||
prefix := map[byte]string{
|
||||
'i': "+ ",
|
||||
'd': "- ",
|
||||
'r': "! ",
|
||||
'e': " ",
|
||||
}
|
||||
|
||||
started := false
|
||||
m := NewMatcher(diff.A, diff.B)
|
||||
for _, g := range m.GetGroupedOpCodes(diff.Context) {
|
||||
if !started {
|
||||
started = true
|
||||
fromDate := ""
|
||||
if len(diff.FromDate) > 0 {
|
||||
fromDate = "\t" + diff.FromDate
|
||||
}
|
||||
toDate := ""
|
||||
if len(diff.ToDate) > 0 {
|
||||
toDate = "\t" + diff.ToDate
|
||||
}
|
||||
if diff.FromFile != "" || diff.ToFile != "" {
|
||||
wf("*** %s%s%s", diff.FromFile, fromDate, diff.Eol)
|
||||
wf("--- %s%s%s", diff.ToFile, toDate, diff.Eol)
|
||||
}
|
||||
}
|
||||
|
||||
first, last := g[0], g[len(g)-1]
|
||||
ws("***************" + diff.Eol)
|
||||
|
||||
range1 := formatRangeContext(first.I1, last.I2)
|
||||
wf("*** %s ****%s", range1, diff.Eol)
|
||||
for _, c := range g {
|
||||
if c.Tag == 'r' || c.Tag == 'd' {
|
||||
for _, cc := range g {
|
||||
if cc.Tag == 'i' {
|
||||
continue
|
||||
}
|
||||
for _, line := range diff.A[cc.I1:cc.I2] {
|
||||
ws(prefix[cc.Tag] + line)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
range2 := formatRangeContext(first.J1, last.J2)
|
||||
wf("--- %s ----%s", range2, diff.Eol)
|
||||
for _, c := range g {
|
||||
if c.Tag == 'r' || c.Tag == 'i' {
|
||||
for _, cc := range g {
|
||||
if cc.Tag == 'd' {
|
||||
continue
|
||||
}
|
||||
for _, line := range diff.B[cc.J1:cc.J2] {
|
||||
ws(prefix[cc.Tag] + line)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return diffErr
|
||||
}
|
||||
|
||||
// Like WriteContextDiff but returns the diff a string.
|
||||
func GetContextDiffString(diff ContextDiff) (string, error) {
|
||||
w := &bytes.Buffer{}
|
||||
err := WriteContextDiff(w, diff)
|
||||
return string(w.Bytes()), err
|
||||
}
|
||||
|
||||
// Split a string on "\n" while preserving them. The output can be used
|
||||
// as input for UnifiedDiff and ContextDiff structures.
|
||||
func SplitLines(s string) []string {
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
go-spew
|
||||
=======
|
||||
|
||||
[](http://copyfree.org)
|
||||
|
||||
Go-spew implements a deep pretty printer for Go data structures to aid in
|
||||
debugging. A comprehensive suite of tests with 100% test coverage is provided
|
||||
to ensure proper functionality.
|
||||
|
||||
## License
|
||||
|
||||
Go-spew is licensed under the [copyfree](http://copyfree.org) ISC License.
|
||||
Generated
Vendored
+1
@@ -18,6 +18,7 @@
|
||||
// tag is deprecated and thus should not be used.
|
||||
// Go versions prior to 1.4 are disabled because they use a different layout
|
||||
// for interfaces which make the implementation of unsafeReflectValue more complex.
|
||||
//go:build !js && !appengine && !safe && !disableunsafe && go1.4
|
||||
// +build !js,!appengine,!safe,!disableunsafe,go1.4
|
||||
|
||||
package spew
|
||||
Generated
Vendored
+1
@@ -16,6 +16,7 @@
|
||||
// when the code is running on Google App Engine, compiled by GopherJS, or
|
||||
// "-tags safe" is added to the go build command line. The "disableunsafe"
|
||||
// tag is deprecated and thus should not be used.
|
||||
//go:build js || appengine || safe || disableunsafe || !go1.4
|
||||
// +build js appengine safe disableunsafe !go1.4
|
||||
|
||||
package spew
|
||||
Generated
Vendored
Generated
Vendored
+15
-15
@@ -254,15 +254,15 @@ pointer addresses used to indirect to the final value. It provides the
|
||||
following features over the built-in printing facilities provided by the fmt
|
||||
package:
|
||||
|
||||
* Pointers are dereferenced and followed
|
||||
* Circular data structures are detected and handled properly
|
||||
* Custom Stringer/error interfaces are optionally invoked, including
|
||||
on unexported types
|
||||
* Custom types which only implement the Stringer/error interfaces via
|
||||
a pointer receiver are optionally invoked when passing non-pointer
|
||||
variables
|
||||
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||
includes offsets, byte values in hex, and ASCII output
|
||||
- Pointers are dereferenced and followed
|
||||
- Circular data structures are detected and handled properly
|
||||
- Custom Stringer/error interfaces are optionally invoked, including
|
||||
on unexported types
|
||||
- Custom types which only implement the Stringer/error interfaces via
|
||||
a pointer receiver are optionally invoked when passing non-pointer
|
||||
variables
|
||||
- Byte arrays and slices are dumped like the hexdump -C command which
|
||||
includes offsets, byte values in hex, and ASCII output
|
||||
|
||||
The configuration options are controlled by modifying the public members
|
||||
of c. See ConfigState for options documentation.
|
||||
@@ -295,12 +295,12 @@ func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{})
|
||||
|
||||
// NewDefaultConfig returns a ConfigState with the following default settings.
|
||||
//
|
||||
// Indent: " "
|
||||
// MaxDepth: 0
|
||||
// DisableMethods: false
|
||||
// DisablePointerMethods: false
|
||||
// ContinueOnMethod: false
|
||||
// SortKeys: false
|
||||
// Indent: " "
|
||||
// MaxDepth: 0
|
||||
// DisableMethods: false
|
||||
// DisablePointerMethods: false
|
||||
// ContinueOnMethod: false
|
||||
// SortKeys: false
|
||||
func NewDefaultConfig() *ConfigState {
|
||||
return &ConfigState{Indent: " "}
|
||||
}
|
||||
Generated
Vendored
+67
-61
@@ -21,35 +21,36 @@ debugging.
|
||||
A quick overview of the additional features spew provides over the built-in
|
||||
printing facilities for Go data types are as follows:
|
||||
|
||||
* Pointers are dereferenced and followed
|
||||
* Circular data structures are detected and handled properly
|
||||
* Custom Stringer/error interfaces are optionally invoked, including
|
||||
on unexported types
|
||||
* Custom types which only implement the Stringer/error interfaces via
|
||||
a pointer receiver are optionally invoked when passing non-pointer
|
||||
variables
|
||||
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||
includes offsets, byte values in hex, and ASCII output (only when using
|
||||
Dump style)
|
||||
- Pointers are dereferenced and followed
|
||||
- Circular data structures are detected and handled properly
|
||||
- Custom Stringer/error interfaces are optionally invoked, including
|
||||
on unexported types
|
||||
- Custom types which only implement the Stringer/error interfaces via
|
||||
a pointer receiver are optionally invoked when passing non-pointer
|
||||
variables
|
||||
- Byte arrays and slices are dumped like the hexdump -C command which
|
||||
includes offsets, byte values in hex, and ASCII output (only when using
|
||||
Dump style)
|
||||
|
||||
There are two different approaches spew allows for dumping Go data structures:
|
||||
|
||||
* Dump style which prints with newlines, customizable indentation,
|
||||
and additional debug information such as types and all pointer addresses
|
||||
used to indirect to the final value
|
||||
* A custom Formatter interface that integrates cleanly with the standard fmt
|
||||
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
|
||||
similar to the default %v while providing the additional functionality
|
||||
outlined above and passing unsupported format verbs such as %x and %q
|
||||
along to fmt
|
||||
- Dump style which prints with newlines, customizable indentation,
|
||||
and additional debug information such as types and all pointer addresses
|
||||
used to indirect to the final value
|
||||
- A custom Formatter interface that integrates cleanly with the standard fmt
|
||||
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
|
||||
similar to the default %v while providing the additional functionality
|
||||
outlined above and passing unsupported format verbs such as %x and %q
|
||||
along to fmt
|
||||
|
||||
Quick Start
|
||||
# Quick Start
|
||||
|
||||
This section demonstrates how to quickly get started with spew. See the
|
||||
sections below for further details on formatting and configuration options.
|
||||
|
||||
To dump a variable with full newlines, indentation, type, and pointer
|
||||
information use Dump, Fdump, or Sdump:
|
||||
|
||||
spew.Dump(myVar1, myVar2, ...)
|
||||
spew.Fdump(someWriter, myVar1, myVar2, ...)
|
||||
str := spew.Sdump(myVar1, myVar2, ...)
|
||||
@@ -58,12 +59,13 @@ Alternatively, if you would prefer to use format strings with a compacted inline
|
||||
printing style, use the convenience wrappers Printf, Fprintf, etc with
|
||||
%v (most compact), %+v (adds pointer addresses), %#v (adds types), or
|
||||
%#+v (adds types and pointer addresses):
|
||||
|
||||
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||
spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||
|
||||
Configuration Options
|
||||
# Configuration Options
|
||||
|
||||
Configuration of spew is handled by fields in the ConfigState type. For
|
||||
convenience, all of the top-level functions use a global state available
|
||||
@@ -74,51 +76,52 @@ equivalent to the top-level functions. This allows concurrent configuration
|
||||
options. See the ConfigState documentation for more details.
|
||||
|
||||
The following configuration options are available:
|
||||
* Indent
|
||||
String to use for each indentation level for Dump functions.
|
||||
It is a single space by default. A popular alternative is "\t".
|
||||
|
||||
* MaxDepth
|
||||
Maximum number of levels to descend into nested data structures.
|
||||
There is no limit by default.
|
||||
- Indent
|
||||
String to use for each indentation level for Dump functions.
|
||||
It is a single space by default. A popular alternative is "\t".
|
||||
|
||||
* DisableMethods
|
||||
Disables invocation of error and Stringer interface methods.
|
||||
Method invocation is enabled by default.
|
||||
- MaxDepth
|
||||
Maximum number of levels to descend into nested data structures.
|
||||
There is no limit by default.
|
||||
|
||||
* DisablePointerMethods
|
||||
Disables invocation of error and Stringer interface methods on types
|
||||
which only accept pointer receivers from non-pointer variables.
|
||||
Pointer method invocation is enabled by default.
|
||||
- DisableMethods
|
||||
Disables invocation of error and Stringer interface methods.
|
||||
Method invocation is enabled by default.
|
||||
|
||||
* DisablePointerAddresses
|
||||
DisablePointerAddresses specifies whether to disable the printing of
|
||||
pointer addresses. This is useful when diffing data structures in tests.
|
||||
- DisablePointerMethods
|
||||
Disables invocation of error and Stringer interface methods on types
|
||||
which only accept pointer receivers from non-pointer variables.
|
||||
Pointer method invocation is enabled by default.
|
||||
|
||||
* DisableCapacities
|
||||
DisableCapacities specifies whether to disable the printing of
|
||||
capacities for arrays, slices, maps and channels. This is useful when
|
||||
diffing data structures in tests.
|
||||
- DisablePointerAddresses
|
||||
DisablePointerAddresses specifies whether to disable the printing of
|
||||
pointer addresses. This is useful when diffing data structures in tests.
|
||||
|
||||
* ContinueOnMethod
|
||||
Enables recursion into types after invoking error and Stringer interface
|
||||
methods. Recursion after method invocation is disabled by default.
|
||||
- DisableCapacities
|
||||
DisableCapacities specifies whether to disable the printing of
|
||||
capacities for arrays, slices, maps and channels. This is useful when
|
||||
diffing data structures in tests.
|
||||
|
||||
* SortKeys
|
||||
Specifies map keys should be sorted before being printed. Use
|
||||
this to have a more deterministic, diffable output. Note that
|
||||
only native types (bool, int, uint, floats, uintptr and string)
|
||||
and types which implement error or Stringer interfaces are
|
||||
supported with other types sorted according to the
|
||||
reflect.Value.String() output which guarantees display
|
||||
stability. Natural map order is used by default.
|
||||
- ContinueOnMethod
|
||||
Enables recursion into types after invoking error and Stringer interface
|
||||
methods. Recursion after method invocation is disabled by default.
|
||||
|
||||
* SpewKeys
|
||||
Specifies that, as a last resort attempt, map keys should be
|
||||
spewed to strings and sorted by those strings. This is only
|
||||
considered if SortKeys is true.
|
||||
- SortKeys
|
||||
Specifies map keys should be sorted before being printed. Use
|
||||
this to have a more deterministic, diffable output. Note that
|
||||
only native types (bool, int, uint, floats, uintptr and string)
|
||||
and types which implement error or Stringer interfaces are
|
||||
supported with other types sorted according to the
|
||||
reflect.Value.String() output which guarantees display
|
||||
stability. Natural map order is used by default.
|
||||
|
||||
Dump Usage
|
||||
- SpewKeys
|
||||
Specifies that, as a last resort attempt, map keys should be
|
||||
spewed to strings and sorted by those strings. This is only
|
||||
considered if SortKeys is true.
|
||||
|
||||
# Dump Usage
|
||||
|
||||
Simply call spew.Dump with a list of variables you want to dump:
|
||||
|
||||
@@ -133,7 +136,7 @@ A third option is to call spew.Sdump to get the formatted output as a string:
|
||||
|
||||
str := spew.Sdump(myVar1, myVar2, ...)
|
||||
|
||||
Sample Dump Output
|
||||
# Sample Dump Output
|
||||
|
||||
See the Dump example for details on the setup of the types and variables being
|
||||
shown here.
|
||||
@@ -150,13 +153,14 @@ shown here.
|
||||
|
||||
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
|
||||
command as shown.
|
||||
|
||||
([]uint8) (len=32 cap=32) {
|
||||
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
|
||||
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
|
||||
00000020 31 32 |12|
|
||||
}
|
||||
|
||||
Custom Formatter
|
||||
# Custom Formatter
|
||||
|
||||
Spew provides a custom formatter that implements the fmt.Formatter interface
|
||||
so that it integrates cleanly with standard fmt package printing functions. The
|
||||
@@ -170,7 +174,7 @@ standard fmt package for formatting. In addition, the custom formatter ignores
|
||||
the width and precision arguments (however they will still work on the format
|
||||
specifiers not handled by the custom formatter).
|
||||
|
||||
Custom Formatter Usage
|
||||
# Custom Formatter Usage
|
||||
|
||||
The simplest way to make use of the spew custom formatter is to call one of the
|
||||
convenience functions such as spew.Printf, spew.Println, or spew.Printf. The
|
||||
@@ -184,15 +188,17 @@ functions have syntax you are most likely already familiar with:
|
||||
|
||||
See the Index for the full list convenience functions.
|
||||
|
||||
Sample Formatter Output
|
||||
# Sample Formatter Output
|
||||
|
||||
Double pointer to a uint8:
|
||||
|
||||
%v: <**>5
|
||||
%+v: <**>(0xf8400420d0->0xf8400420c8)5
|
||||
%#v: (**uint8)5
|
||||
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
|
||||
|
||||
Pointer to circular struct with a uint8 field and a pointer to itself:
|
||||
|
||||
%v: <*>{1 <*><shown>}
|
||||
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>}
|
||||
%#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>}
|
||||
@@ -201,7 +207,7 @@ Pointer to circular struct with a uint8 field and a pointer to itself:
|
||||
See the Printf example for details on the setup of variables being shown
|
||||
here.
|
||||
|
||||
Errors
|
||||
# Errors
|
||||
|
||||
Since it is possible for custom Stringer/error interfaces to panic, spew
|
||||
detects them and handles them internally by printing the panic information
|
||||
Generated
Vendored
+9
-9
@@ -488,15 +488,15 @@ pointer addresses used to indirect to the final value. It provides the
|
||||
following features over the built-in printing facilities provided by the fmt
|
||||
package:
|
||||
|
||||
* Pointers are dereferenced and followed
|
||||
* Circular data structures are detected and handled properly
|
||||
* Custom Stringer/error interfaces are optionally invoked, including
|
||||
on unexported types
|
||||
* Custom types which only implement the Stringer/error interfaces via
|
||||
a pointer receiver are optionally invoked when passing non-pointer
|
||||
variables
|
||||
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||
includes offsets, byte values in hex, and ASCII output
|
||||
- Pointers are dereferenced and followed
|
||||
- Circular data structures are detected and handled properly
|
||||
- Custom Stringer/error interfaces are optionally invoked, including
|
||||
on unexported types
|
||||
- Custom types which only implement the Stringer/error interfaces via
|
||||
a pointer receiver are optionally invoked when passing non-pointer
|
||||
variables
|
||||
- Byte arrays and slices are dumped like the hexdump -C command which
|
||||
includes offsets, byte values in hex, and ASCII output
|
||||
|
||||
The configuration options are controlled by an exported package global,
|
||||
spew.Config. See ConfigState for options documentation.
|
||||
Generated
Vendored
Generated
Vendored
+82
-96
@@ -226,9 +226,9 @@ func yaml_parser_state_machine(parser *yaml_parser_t, event *yaml_event_t) bool
|
||||
}
|
||||
|
||||
// Parse the production:
|
||||
// stream ::= STREAM-START implicit_document? explicit_document* STREAM-END
|
||||
//
|
||||
// ************
|
||||
// stream ::= STREAM-START implicit_document? explicit_document* STREAM-END
|
||||
// ************
|
||||
func yaml_parser_parse_stream_start(parser *yaml_parser_t, event *yaml_event_t) bool {
|
||||
token := peek_token(parser)
|
||||
if token == nil {
|
||||
@@ -249,13 +249,11 @@ func yaml_parser_parse_stream_start(parser *yaml_parser_t, event *yaml_event_t)
|
||||
}
|
||||
|
||||
// Parse the productions:
|
||||
// implicit_document ::= block_node DOCUMENT-END*
|
||||
//
|
||||
// *
|
||||
//
|
||||
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
|
||||
//
|
||||
// *************************
|
||||
// implicit_document ::= block_node DOCUMENT-END*
|
||||
// *
|
||||
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
|
||||
// *************************
|
||||
func yaml_parser_parse_document_start(parser *yaml_parser_t, event *yaml_event_t, implicit bool) bool {
|
||||
|
||||
token := peek_token(parser)
|
||||
@@ -359,9 +357,9 @@ func yaml_parser_parse_document_start(parser *yaml_parser_t, event *yaml_event_t
|
||||
}
|
||||
|
||||
// Parse the productions:
|
||||
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
|
||||
//
|
||||
// ***********
|
||||
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
|
||||
// ***********
|
||||
func yaml_parser_parse_document_content(parser *yaml_parser_t, event *yaml_event_t) bool {
|
||||
token := peek_token(parser)
|
||||
if token == nil {
|
||||
@@ -382,11 +380,10 @@ func yaml_parser_parse_document_content(parser *yaml_parser_t, event *yaml_event
|
||||
}
|
||||
|
||||
// Parse the productions:
|
||||
// implicit_document ::= block_node DOCUMENT-END*
|
||||
//
|
||||
// *************
|
||||
//
|
||||
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
|
||||
// implicit_document ::= block_node DOCUMENT-END*
|
||||
// *************
|
||||
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
|
||||
func yaml_parser_parse_document_end(parser *yaml_parser_t, event *yaml_event_t) bool {
|
||||
token := peek_token(parser)
|
||||
if token == nil {
|
||||
@@ -432,42 +429,32 @@ func yaml_parser_set_event_comments(parser *yaml_parser_t, event *yaml_event_t)
|
||||
}
|
||||
|
||||
// Parse the productions:
|
||||
// block_node_or_indentless_sequence ::=
|
||||
//
|
||||
// ALIAS
|
||||
// *****
|
||||
// | properties (block_content | indentless_block_sequence)?
|
||||
// ********** *
|
||||
// | block_content | indentless_block_sequence
|
||||
// *
|
||||
//
|
||||
// block_node ::= ALIAS
|
||||
//
|
||||
// *****
|
||||
// | properties block_content?
|
||||
// ********** *
|
||||
// | block_content
|
||||
// *
|
||||
//
|
||||
// flow_node ::= ALIAS
|
||||
//
|
||||
// *****
|
||||
// | properties flow_content?
|
||||
// ********** *
|
||||
// | flow_content
|
||||
// *
|
||||
//
|
||||
// properties ::= TAG ANCHOR? | ANCHOR TAG?
|
||||
//
|
||||
// *************************
|
||||
//
|
||||
// block_content ::= block_collection | flow_collection | SCALAR
|
||||
//
|
||||
// ******
|
||||
//
|
||||
// flow_content ::= flow_collection | SCALAR
|
||||
//
|
||||
// ******
|
||||
// block_node_or_indentless_sequence ::=
|
||||
// ALIAS
|
||||
// *****
|
||||
// | properties (block_content | indentless_block_sequence)?
|
||||
// ********** *
|
||||
// | block_content | indentless_block_sequence
|
||||
// *
|
||||
// block_node ::= ALIAS
|
||||
// *****
|
||||
// | properties block_content?
|
||||
// ********** *
|
||||
// | block_content
|
||||
// *
|
||||
// flow_node ::= ALIAS
|
||||
// *****
|
||||
// | properties flow_content?
|
||||
// ********** *
|
||||
// | flow_content
|
||||
// *
|
||||
// properties ::= TAG ANCHOR? | ANCHOR TAG?
|
||||
// *************************
|
||||
// block_content ::= block_collection | flow_collection | SCALAR
|
||||
// ******
|
||||
// flow_content ::= flow_collection | SCALAR
|
||||
// ******
|
||||
func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, indentless_sequence bool) bool {
|
||||
//defer trace("yaml_parser_parse_node", "block:", block, "indentless_sequence:", indentless_sequence)()
|
||||
|
||||
@@ -697,9 +684,9 @@ func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, i
|
||||
}
|
||||
|
||||
// Parse the productions:
|
||||
// block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END
|
||||
//
|
||||
// ******************** *********** * *********
|
||||
// block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END
|
||||
// ******************** *********** * *********
|
||||
func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
|
||||
if first {
|
||||
token := peek_token(parser)
|
||||
@@ -755,9 +742,9 @@ func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_e
|
||||
}
|
||||
|
||||
// Parse the productions:
|
||||
// indentless_sequence ::= (BLOCK-ENTRY block_node?)+
|
||||
//
|
||||
// *********** *
|
||||
// indentless_sequence ::= (BLOCK-ENTRY block_node?)+
|
||||
// *********** *
|
||||
func yaml_parser_parse_indentless_sequence_entry(parser *yaml_parser_t, event *yaml_event_t) bool {
|
||||
token := peek_token(parser)
|
||||
if token == nil {
|
||||
@@ -821,15 +808,15 @@ func yaml_parser_split_stem_comment(parser *yaml_parser_t, stem_len int) {
|
||||
}
|
||||
|
||||
// Parse the productions:
|
||||
// block_mapping ::= BLOCK-MAPPING_START
|
||||
//
|
||||
// *******************
|
||||
// ((KEY block_node_or_indentless_sequence?)?
|
||||
// *** *
|
||||
// (VALUE block_node_or_indentless_sequence?)?)*
|
||||
// block_mapping ::= BLOCK-MAPPING_START
|
||||
// *******************
|
||||
// ((KEY block_node_or_indentless_sequence?)?
|
||||
// *** *
|
||||
// (VALUE block_node_or_indentless_sequence?)?)*
|
||||
//
|
||||
// BLOCK-END
|
||||
// *********
|
||||
// BLOCK-END
|
||||
// *********
|
||||
func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
|
||||
if first {
|
||||
token := peek_token(parser)
|
||||
@@ -896,13 +883,14 @@ func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_even
|
||||
}
|
||||
|
||||
// Parse the productions:
|
||||
// block_mapping ::= BLOCK-MAPPING_START
|
||||
//
|
||||
// ((KEY block_node_or_indentless_sequence?)?
|
||||
// block_mapping ::= BLOCK-MAPPING_START
|
||||
//
|
||||
// (VALUE block_node_or_indentless_sequence?)?)*
|
||||
// ***** *
|
||||
// BLOCK-END
|
||||
// ((KEY block_node_or_indentless_sequence?)?
|
||||
//
|
||||
// (VALUE block_node_or_indentless_sequence?)?)*
|
||||
// ***** *
|
||||
// BLOCK-END
|
||||
func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool {
|
||||
token := peek_token(parser)
|
||||
if token == nil {
|
||||
@@ -929,19 +917,17 @@ func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_ev
|
||||
}
|
||||
|
||||
// Parse the productions:
|
||||
// flow_sequence ::= FLOW-SEQUENCE-START
|
||||
//
|
||||
// *******************
|
||||
// (flow_sequence_entry FLOW-ENTRY)*
|
||||
// * **********
|
||||
// flow_sequence_entry?
|
||||
// *
|
||||
// FLOW-SEQUENCE-END
|
||||
// *****************
|
||||
//
|
||||
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
//
|
||||
// *
|
||||
// flow_sequence ::= FLOW-SEQUENCE-START
|
||||
// *******************
|
||||
// (flow_sequence_entry FLOW-ENTRY)*
|
||||
// * **********
|
||||
// flow_sequence_entry?
|
||||
// *
|
||||
// FLOW-SEQUENCE-END
|
||||
// *****************
|
||||
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
// *
|
||||
func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
|
||||
if first {
|
||||
token := peek_token(parser)
|
||||
@@ -1005,9 +991,9 @@ func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_ev
|
||||
}
|
||||
|
||||
// Parse the productions:
|
||||
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
//
|
||||
// *** *
|
||||
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
// *** *
|
||||
func yaml_parser_parse_flow_sequence_entry_mapping_key(parser *yaml_parser_t, event *yaml_event_t) bool {
|
||||
token := peek_token(parser)
|
||||
if token == nil {
|
||||
@@ -1026,9 +1012,9 @@ func yaml_parser_parse_flow_sequence_entry_mapping_key(parser *yaml_parser_t, ev
|
||||
}
|
||||
|
||||
// Parse the productions:
|
||||
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
//
|
||||
// ***** *
|
||||
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
// ***** *
|
||||
func yaml_parser_parse_flow_sequence_entry_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool {
|
||||
token := peek_token(parser)
|
||||
if token == nil {
|
||||
@@ -1050,9 +1036,9 @@ func yaml_parser_parse_flow_sequence_entry_mapping_value(parser *yaml_parser_t,
|
||||
}
|
||||
|
||||
// Parse the productions:
|
||||
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
//
|
||||
// *
|
||||
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
// *
|
||||
func yaml_parser_parse_flow_sequence_entry_mapping_end(parser *yaml_parser_t, event *yaml_event_t) bool {
|
||||
token := peek_token(parser)
|
||||
if token == nil {
|
||||
@@ -1068,18 +1054,17 @@ func yaml_parser_parse_flow_sequence_entry_mapping_end(parser *yaml_parser_t, ev
|
||||
}
|
||||
|
||||
// Parse the productions:
|
||||
// flow_mapping ::= FLOW-MAPPING-START
|
||||
//
|
||||
// ******************
|
||||
// (flow_mapping_entry FLOW-ENTRY)*
|
||||
// * **********
|
||||
// flow_mapping_entry?
|
||||
// ******************
|
||||
// FLOW-MAPPING-END
|
||||
// ****************
|
||||
//
|
||||
// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
// - *** *
|
||||
// flow_mapping ::= FLOW-MAPPING-START
|
||||
// ******************
|
||||
// (flow_mapping_entry FLOW-ENTRY)*
|
||||
// * **********
|
||||
// flow_mapping_entry?
|
||||
// ******************
|
||||
// FLOW-MAPPING-END
|
||||
// ****************
|
||||
// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
// * *** *
|
||||
func yaml_parser_parse_flow_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
|
||||
if first {
|
||||
token := peek_token(parser)
|
||||
@@ -1144,8 +1129,9 @@ func yaml_parser_parse_flow_mapping_key(parser *yaml_parser_t, event *yaml_event
|
||||
}
|
||||
|
||||
// Parse the productions:
|
||||
// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
// - ***** *
|
||||
//
|
||||
// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
|
||||
// * ***** *
|
||||
func yaml_parser_parse_flow_mapping_value(parser *yaml_parser_t, event *yaml_event_t, empty bool) bool {
|
||||
token := peek_token(parser)
|
||||
if token == nil {
|
||||
|
||||
+20
-24
@@ -433,21 +433,19 @@ type yaml_document_t struct {
|
||||
|
||||
// The prototype of a read handler.
|
||||
//
|
||||
// The read handler is called when the parser needs to read more bytes from the
|
||||
// source. The handler should write not more than size bytes to the buffer.
|
||||
// The number of written bytes should be set to the size_read variable.
|
||||
// The read handler is called when the parser needs to read more bytes from the
|
||||
// source. The handler should write not more than size bytes to the buffer.
|
||||
// The number of written bytes should be set to the size_read variable.
|
||||
//
|
||||
// [in,out] data A pointer to an application data specified by
|
||||
// [in,out] data A pointer to an application data specified by
|
||||
// yaml_parser_set_input().
|
||||
// [out] buffer The buffer to write the data from the source.
|
||||
// [in] size The size of the buffer.
|
||||
// [out] size_read The actual number of bytes read from the source.
|
||||
//
|
||||
// yaml_parser_set_input().
|
||||
//
|
||||
// [out] buffer The buffer to write the data from the source.
|
||||
// [in] size The size of the buffer.
|
||||
// [out] size_read The actual number of bytes read from the source.
|
||||
//
|
||||
// On success, the handler should return 1. If the handler failed,
|
||||
// the returned value should be 0. On EOF, the handler should set the
|
||||
// size_read to 0 and return 1.
|
||||
// On success, the handler should return 1. If the handler failed,
|
||||
// the returned value should be 0. On EOF, the handler should set the
|
||||
// size_read to 0 and return 1.
|
||||
type yaml_read_handler_t func(parser *yaml_parser_t, buffer []byte) (n int, err error)
|
||||
|
||||
// This structure holds information about a potential simple key.
|
||||
@@ -655,19 +653,17 @@ type yaml_comment_t struct {
|
||||
|
||||
// The prototype of a write handler.
|
||||
//
|
||||
// The write handler is called when the emitter needs to flush the accumulated
|
||||
// characters to the output. The handler should write @a size bytes of the
|
||||
// @a buffer to the output.
|
||||
// The write handler is called when the emitter needs to flush the accumulated
|
||||
// characters to the output. The handler should write @a size bytes of the
|
||||
// @a buffer to the output.
|
||||
//
|
||||
// @param[in,out] data A pointer to an application data specified by
|
||||
// @param[in,out] data A pointer to an application data specified by
|
||||
// yaml_emitter_set_output().
|
||||
// @param[in] buffer The buffer with bytes to be written.
|
||||
// @param[in] size The size of the buffer.
|
||||
//
|
||||
// yaml_emitter_set_output().
|
||||
//
|
||||
// @param[in] buffer The buffer with bytes to be written.
|
||||
// @param[in] size The size of the buffer.
|
||||
//
|
||||
// @returns On success, the handler should return @c 1. If the handler failed,
|
||||
// the returned value should be @c 0.
|
||||
// @returns On success, the handler should return @c 1. If the handler failed,
|
||||
// the returned value should be @c 0.
|
||||
type yaml_write_handler_t func(emitter *yaml_emitter_t, buffer []byte) error
|
||||
|
||||
type yaml_emitter_state_t int
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
// Copyright 2025 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.
|
||||
|
||||
//go:build go1.25
|
||||
|
||||
package blake2b
|
||||
|
||||
import "hash"
|
||||
|
||||
var _ hash.XOF = (*xof)(nil)
|
||||
+13
-8
@@ -11,6 +11,7 @@
|
||||
package hkdf
|
||||
|
||||
import (
|
||||
"crypto/hkdf"
|
||||
"crypto/hmac"
|
||||
"errors"
|
||||
"hash"
|
||||
@@ -24,15 +25,19 @@ import (
|
||||
// Expand invocations and different context values. Most common scenarios,
|
||||
// including the generation of multiple keys, should use New instead.
|
||||
func Extract(hash func() hash.Hash, secret, salt []byte) []byte {
|
||||
if salt == nil {
|
||||
salt = make([]byte, hash().Size())
|
||||
// Use the stdlib Extract, which disables FIPS 140 enforcement of the HMAC
|
||||
// key (which in HKDF is the salt). The only possible error is FIPS 140
|
||||
// enforcement of the hash, which had to panic under this API anyway. We
|
||||
// don't use the stdlib Expand, because it switched to returning a []byte
|
||||
// instead of an io.Reader, and Expand uses the HMAC key as a key.
|
||||
out, err := hkdf.Extract(hash, secret, salt)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
extractor := hmac.New(hash, salt)
|
||||
extractor.Write(secret)
|
||||
return extractor.Sum(nil)
|
||||
return out
|
||||
}
|
||||
|
||||
type hkdf struct {
|
||||
type hkdfReader struct {
|
||||
expander hash.Hash
|
||||
size int
|
||||
|
||||
@@ -43,7 +48,7 @@ type hkdf struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (f *hkdf) Read(p []byte) (int, error) {
|
||||
func (f *hkdfReader) Read(p []byte) (int, error) {
|
||||
// Check whether enough data can be generated
|
||||
need := len(p)
|
||||
remains := len(f.buf) + int(255-f.counter+1)*f.size
|
||||
@@ -84,7 +89,7 @@ func (f *hkdf) Read(p []byte) (int, error) {
|
||||
// 3.3. Most common scenarios will want to use New instead.
|
||||
func Expand(hash func() hash.Hash, pseudorandomKey, info []byte) io.Reader {
|
||||
expander := hmac.New(hash, pseudorandomKey)
|
||||
return &hkdf{expander, expander.Size(), info, 1, nil, nil}
|
||||
return &hkdfReader{expander, expander.Size(), info, 1, nil, nil}
|
||||
}
|
||||
|
||||
// New returns a Reader, from which keys can be read, using the given hash,
|
||||
|
||||
+233
-3
@@ -26,6 +26,7 @@ import (
|
||||
"io"
|
||||
"math/big"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
@@ -307,17 +308,50 @@ func parseKey(in []byte) (out *Key, rest []byte, err error) {
|
||||
}, record.Rest, nil
|
||||
}
|
||||
|
||||
// pipelineMaxInFlight is the maximum number of outstanding requests the
|
||||
// client will pipeline to the agent before applying backpressure.
|
||||
const pipelineMaxInFlight = 32
|
||||
|
||||
// client is a client for an ssh-agent process.
|
||||
//
|
||||
// Exactly one of pipeline / (mu, conn) is set, chosen by NewClient
|
||||
// based on whether the underlying transport implements io.Closer.
|
||||
type client struct {
|
||||
// conn is typically a *net.UnixConn
|
||||
// pipeline, if non-nil, dispatches requests over a pipelined
|
||||
// connection: requests are written as soon as the wire is
|
||||
// available and responses are routed back to per-call reply
|
||||
// channels in FIFO order by a background reader goroutine.
|
||||
pipeline *pipeline
|
||||
|
||||
// mu and conn are used in fully-serialized mode, when the
|
||||
// transport does not implement io.Closer. Each call takes mu,
|
||||
// writes its request, reads the matching response, and releases
|
||||
// mu before returning. There is no background goroutine.
|
||||
mu sync.Mutex
|
||||
conn io.ReadWriter
|
||||
// mu is used to prevent concurrent access to the agent
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewClient returns an Agent that talks to an ssh-agent process over
|
||||
// the given connection.
|
||||
//
|
||||
// If rw also implements io.Closer (like *net.UnixConn and ssh.Channel
|
||||
// do), the returned client pipelines concurrent requests over the
|
||||
// connection: callers can issue Sign and other operations from
|
||||
// multiple goroutines and they will be written to the agent as soon
|
||||
// as the wire is available, rather than waiting for the previous
|
||||
// responses. The ssh-agent protocol still requires responses to be
|
||||
// returned in request order, so a slow request delays subsequent
|
||||
// responses on the same connection (head-of-line blocking).
|
||||
//
|
||||
// Pipelining requires io.Closer because, on a Write error, the
|
||||
// background reader goroutine must be unblocked by closing the
|
||||
// underlying connection. When rw does not implement io.Closer
|
||||
// this is not possible, so NewClient falls back to fully
|
||||
// serializing each request: a single in-flight call at a time.
|
||||
func NewClient(rw io.ReadWriter) ExtendedAgent {
|
||||
if rwc, ok := rw.(io.ReadWriteCloser); ok {
|
||||
return &client{pipeline: newPipeline(rwc)}
|
||||
}
|
||||
return &client{conn: rw}
|
||||
}
|
||||
|
||||
@@ -340,6 +374,16 @@ func (c *client) call(req []byte) (reply interface{}, err error) {
|
||||
// bytes of the response are returned; no unmarshalling is
|
||||
// performed on the response.
|
||||
func (c *client) callRaw(req []byte) (reply []byte, err error) {
|
||||
if c.pipeline != nil {
|
||||
return c.pipeline.call(req)
|
||||
}
|
||||
return c.serialCall(req)
|
||||
}
|
||||
|
||||
// serialCall implements the fully-serialized request/response path
|
||||
// used when the transport is not an io.Closer. It writes req under mu
|
||||
// and reads the matching response before returning.
|
||||
func (c *client) serialCall(req []byte) (reply []byte, err error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -577,6 +621,9 @@ func (c *client) insertKey(s interface{}, comment string, constraints []byte) er
|
||||
Constraints: constraints,
|
||||
})
|
||||
case ed25519.PrivateKey:
|
||||
if len(k) != ed25519.PrivateKeySize {
|
||||
return fmt.Errorf("agent: bad ED25519 key size: %d", len(k))
|
||||
}
|
||||
req = ssh.Marshal(ed25519KeyMsg{
|
||||
Type: ssh.KeyAlgoED25519,
|
||||
Pub: []byte(k)[32:],
|
||||
@@ -588,6 +635,9 @@ func (c *client) insertKey(s interface{}, comment string, constraints []byte) er
|
||||
// general idiom is to pass ed25519.PrivateKey by value, not by pointer.
|
||||
// We still support the pointer variant for backwards compatibility.
|
||||
case *ed25519.PrivateKey:
|
||||
if len(*k) != ed25519.PrivateKeySize {
|
||||
return fmt.Errorf("agent: bad ED25519 key size: %d", len(*k))
|
||||
}
|
||||
req = ssh.Marshal(ed25519KeyMsg{
|
||||
Type: ssh.KeyAlgoED25519,
|
||||
Pub: []byte(*k)[32:],
|
||||
@@ -663,6 +713,13 @@ func (c *client) Add(key AddedKey) error {
|
||||
constraints = append(constraints, agentConstrainConfirm)
|
||||
}
|
||||
|
||||
for _, ext := range key.ConstraintExtensions {
|
||||
constraints = append(constraints, ssh.Marshal(constrainExtensionAgentMsg{
|
||||
ExtensionName: ext.ExtensionName,
|
||||
ExtensionDetails: ext.ExtensionDetails,
|
||||
})...)
|
||||
}
|
||||
|
||||
cert := key.Certificate
|
||||
if cert == nil {
|
||||
return c.insertKey(key.PrivateKey, key.Comment, constraints)
|
||||
@@ -705,6 +762,9 @@ func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string
|
||||
Constraints: constraints,
|
||||
})
|
||||
case ed25519.PrivateKey:
|
||||
if len(k) != ed25519.PrivateKeySize {
|
||||
return fmt.Errorf("agent: bad ED25519 key size: %d", len(k))
|
||||
}
|
||||
req = ssh.Marshal(ed25519CertMsg{
|
||||
Type: cert.Type(),
|
||||
CertBytes: cert.Marshal(),
|
||||
@@ -717,6 +777,9 @@ func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string
|
||||
// general idiom is to pass ed25519.PrivateKey by value, not by pointer.
|
||||
// We still support the pointer variant for backwards compatibility.
|
||||
case *ed25519.PrivateKey:
|
||||
if len(*k) != ed25519.PrivateKeySize {
|
||||
return fmt.Errorf("agent: bad ED25519 key size: %d", len(*k))
|
||||
}
|
||||
req = ssh.Marshal(ed25519CertMsg{
|
||||
Type: cert.Type(),
|
||||
CertBytes: cert.Marshal(),
|
||||
@@ -854,3 +917,170 @@ func (c *client) Extension(extensionType string, contents []byte) ([]byte, error
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// pipelineResult carries either a raw agent reply or an error back to a
|
||||
// caller waiting on the response channel.
|
||||
type pipelineResult struct {
|
||||
reply []byte
|
||||
err error
|
||||
}
|
||||
|
||||
// pipeline implements request pipelining over a single agent connection.
|
||||
//
|
||||
// Writers serialize on writeMu to both register a reply channel in the
|
||||
// pending FIFO queue and write the request bytes on the wire; the two
|
||||
// must be atomic so the queue order matches the wire order. A single
|
||||
// reader goroutine decodes responses from the connection and dispatches
|
||||
// each one to the channel at the head of the queue.
|
||||
//
|
||||
// pending is a chan-of-chan acting as a FIFO queue with a fixed
|
||||
// capacity of pipelineMaxInFlight. The outer channel provides ordering
|
||||
// (reads happen in send order) and natural backpressure (a full queue
|
||||
// blocks new writers). Each inner channel is buffered with capacity
|
||||
// one and is sent to exactly once: either by the reader goroutine
|
||||
// with the agent reply, or by shutdown with the terminal error during
|
||||
// drain. The cap-one buffer makes the producer's send non-blocking,
|
||||
// so the reader and shutdown never have to wait for the caller to be
|
||||
// scheduled on the receive.
|
||||
//
|
||||
// When the reader goroutine exits (on read error or protocol
|
||||
// violation), it closes exitCh to wake any writer blocked on the
|
||||
// pending queue, then serializes with any in-flight writer to close
|
||||
// the pending channel, and finally drains the remaining entries
|
||||
// delivering the terminal error to each waiting caller. The
|
||||
// pipeline relies on conn implementing io.Closer so a writer that
|
||||
// hits a Write error can close the connection to unblock the reader
|
||||
// goroutine; NewClient is responsible for only constructing a
|
||||
// pipeline when this guarantee holds.
|
||||
type pipeline struct {
|
||||
conn io.ReadWriteCloser
|
||||
|
||||
writeMu sync.Mutex
|
||||
// pending is the FIFO queue of reply channels with capacity
|
||||
// pipelineMaxInFlight. See type-level documentation.
|
||||
pending chan chan pipelineResult
|
||||
exitCh chan struct{}
|
||||
|
||||
// err carries the terminal error to callers blocked on a closed
|
||||
// pipeline. It is stored exactly once by the reader goroutine
|
||||
// before exitCh is closed; every read happens after observing
|
||||
// exitCh closed, so the load synchronises through the close and
|
||||
// is guaranteed to return the stored value (never nil).
|
||||
err atomic.Pointer[error]
|
||||
}
|
||||
|
||||
func newPipeline(conn io.ReadWriteCloser) *pipeline {
|
||||
p := &pipeline{
|
||||
conn: conn,
|
||||
pending: make(chan chan pipelineResult, pipelineMaxInFlight),
|
||||
exitCh: make(chan struct{}),
|
||||
}
|
||||
go p.readLoop()
|
||||
return p
|
||||
}
|
||||
|
||||
// readLoop decodes responses from conn and dispatches them in FIFO order
|
||||
// to reply channels in pending. On any failure it invokes shutdown.
|
||||
func (p *pipeline) readLoop() {
|
||||
var finalErr error
|
||||
for {
|
||||
var sizeBuf [4]byte
|
||||
if _, err := io.ReadFull(p.conn, sizeBuf[:]); err != nil {
|
||||
finalErr = err
|
||||
break
|
||||
}
|
||||
respSize := binary.BigEndian.Uint32(sizeBuf[:])
|
||||
if respSize > maxAgentResponseBytes {
|
||||
finalErr = errors.New("response too large")
|
||||
break
|
||||
}
|
||||
buf := make([]byte, respSize)
|
||||
if _, err := io.ReadFull(p.conn, buf); err != nil {
|
||||
finalErr = err
|
||||
break
|
||||
}
|
||||
// Successful writes always enqueue before sending bytes, so
|
||||
// pending has a waiting channel for this response.
|
||||
ch := <-p.pending
|
||||
// The reply channel is buffered with capacity 1 and is only
|
||||
// ever written to once, so this send cannot block.
|
||||
ch <- pipelineResult{reply: buf}
|
||||
}
|
||||
p.shutdown(clientErr(finalErr))
|
||||
}
|
||||
|
||||
// shutdown is called exactly once, from readLoop, when the reader is
|
||||
// terminating. It unblocks pending writers and fails all in-flight
|
||||
// requests with finalErr.
|
||||
func (p *pipeline) shutdown(finalErr error) {
|
||||
// Publish the terminal error before closing exitCh so any
|
||||
// writer that subsequently observes exitCh closed sees err.
|
||||
p.err.Store(&finalErr)
|
||||
|
||||
// Wake any writer blocked waiting for a slot in the pending queue.
|
||||
close(p.exitCh)
|
||||
|
||||
// Wait for any writer currently inside its critical section to
|
||||
// complete. After this lock, no new writer can reach the send on
|
||||
// pending: they will observe exitCh closed in the select and bail
|
||||
// out before attempting the send.
|
||||
p.writeMu.Lock()
|
||||
close(p.pending)
|
||||
p.writeMu.Unlock()
|
||||
|
||||
// Drain entries that were enqueued but never answered, delivering
|
||||
// the terminal error to their waiting callers. The reply channels
|
||||
// are buffered (cap 1) and written to exactly once, so these sends
|
||||
// cannot block.
|
||||
for ch := range p.pending {
|
||||
ch <- pipelineResult{err: finalErr}
|
||||
}
|
||||
}
|
||||
|
||||
// call sends req to the agent and returns the matching raw response.
|
||||
func (p *pipeline) call(req []byte) ([]byte, error) {
|
||||
replyCh := make(chan pipelineResult, 1)
|
||||
|
||||
p.writeMu.Lock()
|
||||
|
||||
// Priority check: if the reader has already finished shutdown,
|
||||
// pending is closed and sending to it would panic. Bail out now.
|
||||
// Once we pass this check while holding writeMu, shutdown cannot
|
||||
// complete close(pending) until we release writeMu, so the send
|
||||
// below is safe against concurrent closure.
|
||||
select {
|
||||
case <-p.exitCh:
|
||||
p.writeMu.Unlock()
|
||||
return nil, *p.err.Load()
|
||||
default:
|
||||
}
|
||||
|
||||
// Enqueue the reply channel before writing the request, so FIFO
|
||||
// order on the wire matches FIFO order in the pending queue. The
|
||||
// exitCh arm handles the case where the reader errors while we
|
||||
// block on a full queue.
|
||||
select {
|
||||
case p.pending <- replyCh:
|
||||
case <-p.exitCh:
|
||||
p.writeMu.Unlock()
|
||||
return nil, *p.err.Load()
|
||||
}
|
||||
|
||||
msg := make([]byte, 4+len(req))
|
||||
binary.BigEndian.PutUint32(msg, uint32(len(req)))
|
||||
copy(msg[4:], req)
|
||||
_, werr := p.conn.Write(msg)
|
||||
p.writeMu.Unlock()
|
||||
|
||||
if werr != nil {
|
||||
// The connection is in an undefined state. Close it so the
|
||||
// reader unblocks promptly and triggers shutdown for every
|
||||
// other in-flight caller. NewClient guarantees conn is a
|
||||
// real io.Closer when the pipeline is in use.
|
||||
p.conn.Close()
|
||||
return nil, clientErr(werr)
|
||||
}
|
||||
|
||||
res := <-replyCh
|
||||
return res.reply, res.err
|
||||
}
|
||||
|
||||
+18
-5
@@ -32,8 +32,10 @@ type keyring struct {
|
||||
|
||||
var errLocked = errors.New("agent: locked")
|
||||
|
||||
// NewKeyring returns an Agent that holds keys in memory. It is safe
|
||||
// for concurrent use by multiple goroutines.
|
||||
// NewKeyring returns an Agent that holds keys in memory. It is safe for
|
||||
// concurrent use by multiple goroutines.
|
||||
//
|
||||
// The returned Agent only supports the "lifetime" constraint.
|
||||
func NewKeyring() Agent {
|
||||
return &keyring{}
|
||||
}
|
||||
@@ -143,15 +145,26 @@ func (r *keyring) List() ([]*Key, error) {
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// Insert adds a private key to the keyring. If a certificate
|
||||
// is given, that certificate is added as public key. Note that
|
||||
// any constraints given are ignored.
|
||||
// Add adds a private key to the keyring. If a certificate is given, that
|
||||
// certificate is added as public key.
|
||||
//
|
||||
// Add returns an error if key contains ConstraintExtensions or
|
||||
// ConfirmBeforeUse.
|
||||
func (r *keyring) Add(key AddedKey) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.locked {
|
||||
return errLocked
|
||||
}
|
||||
|
||||
if key.ConfirmBeforeUse {
|
||||
return errors.New("agent: confirm before use constraint is not supported")
|
||||
}
|
||||
|
||||
if len(key.ConstraintExtensions) > 0 {
|
||||
return errors.New("agent: constraint extensions are present but not supported")
|
||||
}
|
||||
|
||||
signer, err := ssh.NewSignerFromKey(key.PrivateKey)
|
||||
|
||||
if err != nil {
|
||||
|
||||
+32
-4
@@ -240,13 +240,35 @@ func setConstraints(key *AddedKey, constraintBytes []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkRSAKeyParams enforces the same bounds as parseRSA in the ssh
|
||||
// package, and additionally caps the prime factors. Without this,
|
||||
// the rsa.PrivateKey built from an Add request would call Precompute()
|
||||
// on arbitrary inputs; the CRT coefficient recomputation is cubic in
|
||||
// |p| and can consume excessive CPU on oversized keys.
|
||||
func checkRSAKeyParams(N, E, P, Q *big.Int) error {
|
||||
if N.BitLen() > 8192 {
|
||||
return errors.New("agent: RSA modulus too large")
|
||||
}
|
||||
if P.BitLen() > 4096 || Q.BitLen() > 4096 {
|
||||
return errors.New("agent: RSA prime too large")
|
||||
}
|
||||
if E.BitLen() > 24 {
|
||||
return errors.New("agent: RSA public exponent too large")
|
||||
}
|
||||
e := E.Int64()
|
||||
if e < 3 || e&1 == 0 {
|
||||
return errors.New("agent: incorrect RSA public exponent")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseRSAKey(req []byte) (*AddedKey, error) {
|
||||
var k rsaKeyMsg
|
||||
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if k.E.BitLen() > 30 {
|
||||
return nil, errors.New("agent: RSA public exponent too large")
|
||||
if err := checkRSAKeyParams(k.N, k.E, k.P, k.Q); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priv := &rsa.PrivateKey{
|
||||
PublicKey: rsa.PublicKey{
|
||||
@@ -270,6 +292,9 @@ func parseEd25519Key(req []byte) (*AddedKey, error) {
|
||||
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(k.Priv) != ed25519.PrivateKeySize {
|
||||
return nil, fmt.Errorf("agent: bad ED25519 key size: %d", len(k.Priv))
|
||||
}
|
||||
priv := ed25519.PrivateKey(k.Priv)
|
||||
|
||||
addedKey := &AddedKey{PrivateKey: &priv, Comment: k.Comments}
|
||||
@@ -336,6 +361,9 @@ func parseEd25519Cert(req []byte) (*AddedKey, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(k.Priv) != ed25519.PrivateKeySize {
|
||||
return nil, fmt.Errorf("agent: bad ED25519 key size: %d", len(k.Priv))
|
||||
}
|
||||
priv := ed25519.PrivateKey(k.Priv)
|
||||
cert, ok := pubKey.(*ssh.Certificate)
|
||||
if !ok {
|
||||
@@ -393,8 +421,8 @@ func parseRSACert(req []byte) (*AddedKey, error) {
|
||||
return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err)
|
||||
}
|
||||
|
||||
if rsaPub.E.BitLen() > 30 {
|
||||
return nil, errors.New("agent: RSA public exponent too large")
|
||||
if err := checkRSAKeyParams(rsaPub.N, rsaPub.E, k.P, k.Q); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
priv := rsa.PrivateKey{
|
||||
|
||||
+17
-1
@@ -348,6 +348,9 @@ func (c *CertChecker) CheckHostKey(addr string, remote net.Addr, key PublicKey)
|
||||
if cert.CertType != HostCert {
|
||||
return fmt.Errorf("ssh: certificate presented as a host key has type %d", cert.CertType)
|
||||
}
|
||||
if c.IsHostAuthority == nil {
|
||||
return errors.New("ssh: cannot verify certificate, IsHostAuthority not set")
|
||||
}
|
||||
if !c.IsHostAuthority(cert.SignatureKey, addr) {
|
||||
return fmt.Errorf("ssh: no authorities for hostname: %v", addr)
|
||||
}
|
||||
@@ -375,6 +378,9 @@ func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permis
|
||||
if cert.CertType != UserCert {
|
||||
return nil, fmt.Errorf("ssh: cert has type %d", cert.CertType)
|
||||
}
|
||||
if c.IsUserAuthority == nil {
|
||||
return nil, errors.New("ssh: cannot verify certificate, IsUserAuthority not set")
|
||||
}
|
||||
if !c.IsUserAuthority(cert.SignatureKey) {
|
||||
return nil, fmt.Errorf("ssh: certificate signed by unrecognized authority")
|
||||
}
|
||||
@@ -438,7 +444,17 @@ func (c *CertChecker) CheckCert(principal string, cert *Certificate) error {
|
||||
if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(CertTimeInfinity) && (unixNow >= before || before < 0) {
|
||||
return fmt.Errorf("ssh: cert has expired")
|
||||
}
|
||||
if err := cert.SignatureKey.Verify(cert.bytesForSigning(), cert.Signature); err != nil {
|
||||
// Match OpenSSH: the SK user-presence flag is never enforced on a
|
||||
// certificate's CA signature. OpenSSH calls sshkey_verify with
|
||||
// detailsp==NULL in sshkey.c:cert_parse, so the UP/UV flags are
|
||||
// not even extracted. The UP bit on a CA signature reflects the
|
||||
// CA operator's presence at signing time, which has no bearing on
|
||||
// whether the user being authenticated is present now; enforcing
|
||||
// it here would only break interop with certificates issued by
|
||||
// non-interactive SK CAs. skKeyWithoutUP is a no-op for non-SK
|
||||
// keys (the common case).
|
||||
caKey := skKeyWithoutUP(cert.SignatureKey)
|
||||
if err := caKey.Verify(cert.bytesForSigning(), cert.Signature); err != nil {
|
||||
return fmt.Errorf("ssh: certificate signature does not verify")
|
||||
}
|
||||
|
||||
|
||||
+62
-6
@@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -131,11 +132,17 @@ func (r RejectionReason) String() string {
|
||||
return fmt.Sprintf("unknown reason %d", int(r))
|
||||
}
|
||||
|
||||
func min(a uint32, b int) uint32 {
|
||||
if a < uint32(b) {
|
||||
return a
|
||||
// minPayloadSize returns min(limit, length) clamped to a uint32. It is used
|
||||
// to compute the size of the next channel data packet from the remaining
|
||||
// payload. The comparison is done in int64 because length is an int — on
|
||||
// 64-bit systems len(data) can exceed 2^32, and a direct uint32(length)
|
||||
// cast would silently truncate to 0 at every multiple of 2^32, causing
|
||||
// WriteExtended's loop to spin without making progress.
|
||||
func minPayloadSize(limit uint32, length int) uint32 {
|
||||
if int64(length) > int64(limit) {
|
||||
return limit
|
||||
}
|
||||
return uint32(b)
|
||||
return uint32(length)
|
||||
}
|
||||
|
||||
type channelDirection uint8
|
||||
@@ -177,6 +184,12 @@ type channel struct {
|
||||
// with WantReply=true outstanding. This lock is held by a
|
||||
// goroutine that has such an outgoing request pending.
|
||||
sentRequestMu sync.Mutex
|
||||
// sentRequestPending is set to true while a SendRequest call with
|
||||
// WantReply=true is in flight. handlePacket uses it as a gate: responses
|
||||
// arriving while no request is pending are dropped to prevent a
|
||||
// misbehaving peer from stalling the mux read loop by filling ch.msg
|
||||
// with unsolicited channelRequestSuccess/Failure messages.
|
||||
sentRequestPending atomic.Bool
|
||||
|
||||
incomingRequests chan *Request
|
||||
|
||||
@@ -251,7 +264,7 @@ func (ch *channel) WriteExtended(data []byte, extendedCode uint32) (n int, err e
|
||||
ch.writeMu.Unlock()
|
||||
|
||||
for len(data) > 0 {
|
||||
space := min(ch.maxRemotePayload, len(data))
|
||||
space := minPayloadSize(ch.maxRemotePayload, len(data))
|
||||
if space, err = ch.remoteWin.reserve(space); err != nil {
|
||||
return n, err
|
||||
}
|
||||
@@ -460,6 +473,18 @@ func (ch *channel) handlePacket(packet []byte) error {
|
||||
}
|
||||
|
||||
ch.incomingRequests <- &req
|
||||
case *channelRequestSuccessMsg, *channelRequestFailureMsg:
|
||||
// Drop responses that arrive when no SendRequest is waiting, to
|
||||
// prevent a malicious peer from filling ch.msg and stalling the
|
||||
// mux read loop. The non-blocking send additionally protects the
|
||||
// loop if a well-behaved caller is slow to read.
|
||||
if !ch.sentRequestPending.Load() {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case ch.msg <- msg:
|
||||
default:
|
||||
}
|
||||
default:
|
||||
ch.msg <- msg
|
||||
}
|
||||
@@ -530,7 +555,17 @@ func (ch *channel) Reject(reason RejectionReason, message string) error {
|
||||
Language: "en",
|
||||
}
|
||||
ch.decided = true
|
||||
return ch.sendMessage(reject)
|
||||
err := ch.sendMessage(reject)
|
||||
|
||||
// Remove the channel from the mux to prevent memory leaks.
|
||||
// Do not call ch.close() here: no goroutine holds a reference to a
|
||||
// rejected channel's internal channels (msg, incomingRequests), so
|
||||
// removing it from chanList is sufficient for GC. Calling close()
|
||||
// would race with the mux loop goroutine (handlePacket or dropAll),
|
||||
// causing a panic from closing an already-closed channel.
|
||||
ch.mux.chanList.remove(ch.localId)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (ch *channel) Read(data []byte) (int, error) {
|
||||
@@ -586,6 +621,27 @@ func (ch *channel) SendRequest(name string, wantReply bool, payload []byte) (boo
|
||||
if wantReply {
|
||||
ch.sentRequestMu.Lock()
|
||||
defer ch.sentRequestMu.Unlock()
|
||||
|
||||
// Open the gate so that responses arriving while this request is in
|
||||
// flight are allowed to reach ch.msg. Responses arriving while no
|
||||
// request is pending are dropped by handlePacket.
|
||||
ch.sentRequestPending.Store(true)
|
||||
defer ch.sentRequestPending.Store(false)
|
||||
|
||||
// Drain any spurious responses that may have been buffered. This
|
||||
// prevents a previously buffered unexpected response from being
|
||||
// consumed instead of the actual response for this request.
|
||||
drain:
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-ch.msg:
|
||||
if !ok {
|
||||
break drain
|
||||
}
|
||||
default:
|
||||
break drain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg := channelRequestMsg{
|
||||
|
||||
+1
-1
@@ -407,7 +407,7 @@ func (c *gcmCipher) readCipherPacket(seqNum uint32, r io.Reader) ([]byte, error)
|
||||
return nil, fmt.Errorf("ssh: illegal padding %d", padding)
|
||||
}
|
||||
|
||||
if int(padding+1) >= len(plain) {
|
||||
if int(padding)+1 >= len(plain) {
|
||||
return nil, fmt.Errorf("ssh: padding %d too large", padding)
|
||||
}
|
||||
plain = plain[1 : length-uint32(padding)]
|
||||
|
||||
+85
@@ -88,6 +88,32 @@ func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan
|
||||
return conn, conn.mux.incomingChannels, conn.mux.incomingRequests, nil
|
||||
}
|
||||
|
||||
// NewControlClientConn establishes an SSH connection over an OpenSSH
|
||||
// ControlMaster socket c in proxy mode.
|
||||
//
|
||||
// Note that this package only implements the client side of the multiplexing
|
||||
// protocol. The provided net.Conn must be a local, secure connection (such as a
|
||||
// Unix domain socket) connected to an already-running OpenSSH process acting as
|
||||
// the ControlMaster.
|
||||
//
|
||||
// WARNING: Because proxy mode bypasses the standard cryptographic handshake
|
||||
// passing a standard network connection (e.g., TCP) will result in plaintext
|
||||
// data leakage.
|
||||
//
|
||||
// The Request and NewChannel channels must be serviced or the connection
|
||||
// will hang.
|
||||
func NewControlClientConn(c net.Conn) (Conn, <-chan NewChannel, <-chan *Request, error) {
|
||||
conn := &connection{
|
||||
sshConn: sshConn{conn: c},
|
||||
}
|
||||
var err error
|
||||
if conn.transport, err = handshakeControlProxy(c); err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("ssh: control proxy handshake failed: %w", err)
|
||||
}
|
||||
conn.mux = newMux(conn.transport)
|
||||
return conn, conn.mux.incomingChannels, conn.mux.incomingRequests, nil
|
||||
}
|
||||
|
||||
// clientHandshake performs the client side key exchange. See RFC 4253 Section
|
||||
// 7.
|
||||
func (c *connection) clientHandshake(dialAddress string, config *ClientConfig) error {
|
||||
@@ -197,6 +223,59 @@ type HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
|
||||
// the server. A BannerCallback receives the message sent by the remote server.
|
||||
type BannerCallback func(message string) error
|
||||
|
||||
// ClientAuthContext contains information about the current state of the
|
||||
// authentication process, passed to [ClientAuthCallback].
|
||||
type ClientAuthContext struct {
|
||||
// Metadata contains the connection metadata.
|
||||
Metadata ConnMetadata
|
||||
|
||||
// Algorithms contains the negotiated algorithms.
|
||||
Algorithms NegotiatedAlgorithms
|
||||
|
||||
// AllowedMethods lists the authentication methods currently accepted
|
||||
// by the server. These are the protocol-level names defined in RFC 4252
|
||||
// such as "publickey", "password".
|
||||
AllowedMethods []string
|
||||
|
||||
// PartialSuccessMethods lists the authentication methods that have already
|
||||
// succeeded, indicating a multi-step authentication flow. This list
|
||||
// represents the exact sequence of partial successes and may contain
|
||||
// duplicates if the same method succeeded multiple times.
|
||||
PartialSuccessMethods []string
|
||||
|
||||
// TriedMethods lists the methods that have already been attempted and
|
||||
// failed during this session. This list represents the exact sequence of
|
||||
// failures and may contain duplicates. This allows the callback to also
|
||||
// track the number of failed attempts for a specific method.
|
||||
TriedMethods []string
|
||||
}
|
||||
|
||||
// ClientAuthCallback is a hook invoked before each authentication attempt. It
|
||||
// allows the client to dynamically select an authentication method based on the
|
||||
// current context, server capabilities, or previous failures.
|
||||
//
|
||||
// The callback is invoked after the initial "none" authentication method, once
|
||||
// the server's supported authentication methods are known.
|
||||
//
|
||||
// Return values:
|
||||
// - (AuthMethod, nil): The client will attempt this specific method next.
|
||||
// The returned method does NOT need to be present in [ClientConfig.Auth].
|
||||
// This allows for dynamic authentication strategies (e.g., prompting
|
||||
// for a password only if public key auth fails). Callers should inspect
|
||||
// [ClientAuthContext.TriedMethods] to avoid repeatedly returning the
|
||||
// same failing method.
|
||||
// - (nil, nil): The client selects from [ClientConfig.Auth] the first
|
||||
// instance of a method that has not been tried yet, or aborts if none
|
||||
// are left. If authentication is not successful, the callback is invoked
|
||||
// again before the following attempt.
|
||||
// - (nil, error): The authentication process is aborted immediately,
|
||||
// causing the ongoing SSH handshake to fail with the provided error.
|
||||
//
|
||||
// To bound resource use, the client caps the total number of authentication
|
||||
// attempts (failures and partial successes combined) at 64. If the cap is
|
||||
// exceeded the handshake aborts with an error.
|
||||
type ClientAuthCallback func(ctx *ClientAuthContext) (AuthMethod, error)
|
||||
|
||||
// A ClientConfig structure is used to configure a Client. It must not be
|
||||
// modified after having been passed to an SSH function.
|
||||
type ClientConfig struct {
|
||||
@@ -210,6 +289,9 @@ type ClientConfig struct {
|
||||
// Auth contains possible authentication methods to use with the
|
||||
// server. Only the first instance of a particular RFC 4252 method will
|
||||
// be used during authentication.
|
||||
//
|
||||
// If AuthCallback is set, these AuthMethod are only used if the
|
||||
// callback returns nil.
|
||||
Auth []AuthMethod
|
||||
|
||||
// HostKeyCallback is called during the cryptographic
|
||||
@@ -240,6 +322,9 @@ type ClientConfig struct {
|
||||
//
|
||||
// A Timeout of zero means no timeout.
|
||||
Timeout time.Duration
|
||||
|
||||
// AuthCallback, if non-nil, is invoked before each authentication attempt.
|
||||
AuthCallback ClientAuthCallback
|
||||
}
|
||||
|
||||
// InsecureIgnoreHostKey returns a function that can be used for
|
||||
|
||||
+50
-14
@@ -21,6 +21,12 @@ const (
|
||||
authSuccess
|
||||
)
|
||||
|
||||
// maxAuthClientTried bounds the total number of authentication attempts
|
||||
// (failures and partial successes combined) the client makes before
|
||||
// aborting the loop, to prevent unbounded growth when an AuthCallback
|
||||
// keeps supplying methods.
|
||||
const maxAuthClientTried = 64
|
||||
|
||||
// clientAuthenticate authenticates with the remote server. See RFC 4252.
|
||||
func (c *connection) clientAuthenticate(config *ClientConfig) error {
|
||||
// initiate user auth session
|
||||
@@ -67,32 +73,62 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
|
||||
// then any untried methods suggested by the server.
|
||||
var tried []string
|
||||
var lastMethods []string
|
||||
var partialSuccess []string
|
||||
|
||||
sessionID := c.transport.getSessionID()
|
||||
for auth := AuthMethod(new(noneAuth)); auth != nil; {
|
||||
ok, methods, err := auth.auth(sessionID, config.User, c.transport, config.Rand, extensions)
|
||||
if err != nil {
|
||||
// On disconnect, return error immediately
|
||||
if _, ok := err.(*disconnectMsg); ok {
|
||||
if _, isDisconnect := err.(*disconnectMsg); isDisconnect {
|
||||
return err
|
||||
}
|
||||
// We return the error later if there is no other method left to
|
||||
// try.
|
||||
// We return the error later if there is no other method
|
||||
// left to try.
|
||||
ok = authFailure
|
||||
}
|
||||
if ok == authSuccess {
|
||||
// success
|
||||
|
||||
switch ok {
|
||||
case authSuccess:
|
||||
return nil
|
||||
} else if ok == authFailure {
|
||||
if m := auth.method(); !slices.Contains(tried, m) {
|
||||
tried = append(tried, m)
|
||||
}
|
||||
case authPartialSuccess:
|
||||
partialSuccess = append(partialSuccess, auth.method())
|
||||
case authFailure:
|
||||
tried = append(tried, auth.method())
|
||||
}
|
||||
if len(partialSuccess)+len(tried) > maxAuthClientTried {
|
||||
return fmt.Errorf("ssh: too many authentication attempts (%d), aborting",
|
||||
len(partialSuccess)+len(tried))
|
||||
}
|
||||
|
||||
if methods == nil {
|
||||
methods = lastMethods
|
||||
}
|
||||
lastMethods = methods
|
||||
|
||||
// If AuthCallback is set it takes precedence: it picks the next
|
||||
// AuthMethod dynamically. The returned method need not be in
|
||||
// config.Auth. If the callback returns (nil, nil) we fall back to
|
||||
// selecting the next untried method from config.Auth below; on
|
||||
// (nil, error) the handshake aborts.
|
||||
if config.AuthCallback != nil {
|
||||
ctx := &ClientAuthContext{
|
||||
Metadata: c,
|
||||
Algorithms: c.Algorithms(),
|
||||
AllowedMethods: slices.Clone(methods),
|
||||
PartialSuccessMethods: slices.Clone(partialSuccess),
|
||||
TriedMethods: slices.Clone(tried),
|
||||
}
|
||||
altAuth, cbErr := config.AuthCallback(ctx)
|
||||
if cbErr != nil {
|
||||
return cbErr
|
||||
}
|
||||
if altAuth != nil {
|
||||
auth = altAuth
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
auth = nil
|
||||
|
||||
findNext:
|
||||
@@ -377,11 +413,11 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
|
||||
return authFailure, nil, err
|
||||
}
|
||||
|
||||
// If authentication succeeds or the list of available methods does not
|
||||
// contain the "publickey" method, do not attempt to authenticate with any
|
||||
// other keys. According to RFC 4252 Section 7, the latter can occur when
|
||||
// additional authentication methods are required.
|
||||
if success == authSuccess || !slices.Contains(methods, cb.method()) {
|
||||
// If authentication succeeds or partially succeeds, return immediately
|
||||
// so the caller can select the next auth method. According to RFC 4252
|
||||
// Section 7, if the server no longer lists "publickey" among its
|
||||
// allowed methods, do not attempt to authenticate with any other keys.
|
||||
if success == authSuccess || success == authPartialSuccess || !slices.Contains(methods, cb.method()) {
|
||||
return success, methods, err
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -91,9 +91,17 @@ func DiscardRequests(in <-chan *Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// A connTransport represents the transport for a connection.
|
||||
type connTransport interface {
|
||||
packetConn
|
||||
getAlgorithms() NegotiatedAlgorithms
|
||||
getSessionID() []byte
|
||||
waitSession() error
|
||||
}
|
||||
|
||||
// A connection represents an incoming connection.
|
||||
type connection struct {
|
||||
transport *handshakeTransport
|
||||
transport connTransport
|
||||
sshConn
|
||||
|
||||
// The connection protocol.
|
||||
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
// Copyright 2026 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
)
|
||||
|
||||
const (
|
||||
muxProtocolVersion = 4
|
||||
|
||||
muxMsgHello = 0x00000001
|
||||
muxCProxy = 0x1000000f
|
||||
muxSProxy = 0x8000000f
|
||||
)
|
||||
|
||||
const controlProxyRequestID = 0
|
||||
|
||||
// handshakeControlProxy attempts to establish a transport connection with an
|
||||
// OpenSSH ControlMaster socket in proxy mode. For details see:
|
||||
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.mux
|
||||
func handshakeControlProxy(rw io.ReadWriteCloser) (connTransport, error) {
|
||||
if err := controlProxyWritePacket(rw, func(b *cryptobyte.Builder) {
|
||||
b.AddUint32(muxMsgHello)
|
||||
b.AddUint32(muxProtocolVersion)
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("mux hello write failed: %w", err)
|
||||
}
|
||||
if err := controlProxyWritePacket(rw, func(b *cryptobyte.Builder) {
|
||||
b.AddUint32(muxCProxy)
|
||||
b.AddUint32(controlProxyRequestID)
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("mux client proxy write failed: %w", err)
|
||||
}
|
||||
|
||||
messageType, body, err := controlProxyReadMessage(rw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mux hello read failed: %w", err)
|
||||
}
|
||||
if messageType != muxMsgHello {
|
||||
return nil, fmt.Errorf("expected hello response, got %v", messageType)
|
||||
}
|
||||
var v uint32
|
||||
if !body.ReadUint32(&v) {
|
||||
return nil, errors.New("EOF reading mux protocol version")
|
||||
}
|
||||
if v != muxProtocolVersion {
|
||||
return nil, fmt.Errorf("mux server has unsupported version %v", v)
|
||||
}
|
||||
messageType, body, err = controlProxyReadMessage(rw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mux server proxy read failed: %w", err)
|
||||
}
|
||||
if messageType != muxSProxy {
|
||||
return nil, fmt.Errorf("expected server proxy response, got %v", messageType)
|
||||
}
|
||||
var reqID uint32
|
||||
if !body.ReadUint32(&reqID) {
|
||||
return nil, errors.New("EOF reading request id")
|
||||
}
|
||||
if reqID != controlProxyRequestID {
|
||||
return nil, fmt.Errorf("expected request id %v, got %v", controlProxyRequestID, reqID)
|
||||
}
|
||||
return &controlProxyTransport{rw}, nil
|
||||
}
|
||||
|
||||
// controlProxyTransport implements the connTransport interface for
|
||||
// ControlMaster connections. Each controlMessage has zero length padding and
|
||||
// no MAC.
|
||||
type controlProxyTransport struct {
|
||||
rw io.ReadWriteCloser
|
||||
}
|
||||
|
||||
func (p *controlProxyTransport) Close() error {
|
||||
return p.rw.Close()
|
||||
}
|
||||
|
||||
func (p *controlProxyTransport) writePacket(controlMessage []byte) error {
|
||||
return controlProxyWritePacket(p.rw, func(b *cryptobyte.Builder) {
|
||||
b.AddUint8(0) // Padding length.
|
||||
b.AddBytes(controlMessage)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *controlProxyTransport) readPacket() ([]byte, error) {
|
||||
buf, err := controlProxyReadPacket(p.rw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssh: error reading control message: %w", err)
|
||||
}
|
||||
// Discard the padding length.
|
||||
if len(buf) < 1 {
|
||||
return nil, errors.New("ssh: EOF reading padding length")
|
||||
}
|
||||
if buf[0] != 0 {
|
||||
return nil, errors.New("ssh: unexpected non-zero padding in control message")
|
||||
}
|
||||
return buf[1:], nil
|
||||
}
|
||||
|
||||
func (p *controlProxyTransport) getAlgorithms() NegotiatedAlgorithms {
|
||||
return NegotiatedAlgorithms{}
|
||||
}
|
||||
|
||||
func (p *controlProxyTransport) getSessionID() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *controlProxyTransport) waitSession() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func controlProxyWritePacket(w io.Writer, f cryptobyte.BuilderContinuation) error {
|
||||
var buf []byte
|
||||
b := cryptobyte.NewBuilder(buf)
|
||||
b.AddUint32LengthPrefixed(f)
|
||||
out, err := b.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func controlProxyReadPacket(r io.Reader) (cryptobyte.String, error) {
|
||||
var l uint32
|
||||
if err := binary.Read(r, binary.BigEndian, &l); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if l > maxPacket {
|
||||
return nil, fmt.Errorf("message length %v exceeds maximum %v", l, maxPacket)
|
||||
}
|
||||
buf := make([]byte, l)
|
||||
if _, err := io.ReadFull(r, buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func controlProxyReadMessage(r io.Reader) (messageType uint32, body cryptobyte.String, err error) {
|
||||
body, err = controlProxyReadPacket(r)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("error reading message body: %w", err)
|
||||
}
|
||||
if !body.ReadUint32(&messageType) {
|
||||
return 0, nil, errors.New("EOF reading message type")
|
||||
}
|
||||
return messageType, body, nil
|
||||
}
|
||||
+66
-9
@@ -16,6 +16,7 @@ import (
|
||||
"io"
|
||||
"math/big"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
@@ -718,15 +719,9 @@ func (gex *dhGEXSHA) Server(c packetConn, randSource io.Reader, magics *handshak
|
||||
kexDHGexRequest.MaxBits, kexDHGexRequest.PreferredBits)
|
||||
}
|
||||
|
||||
var p *big.Int
|
||||
// We hardcode sending Oakley Group 14 (2048 bits), Oakley Group 15 (3072
|
||||
// bits) or Oakley Group 16 (4096 bits), based on the requested max size.
|
||||
if kexDHGexRequest.MaxBits < 3072 {
|
||||
p, _ = new(big.Int).SetString(oakleyGroup14, 16)
|
||||
} else if kexDHGexRequest.MaxBits < 4096 {
|
||||
p, _ = new(big.Int).SetString(oakleyGroup15, 16)
|
||||
} else {
|
||||
p, _ = new(big.Int).SetString(oakleyGroup16, 16)
|
||||
p, err := chooseDH(kexDHGexRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g := big.NewInt(2)
|
||||
@@ -805,3 +800,65 @@ func (gex *dhGEXSHA) Server(c packetConn, randSource io.Reader, magics *handshak
|
||||
Hash: gex.hashFunc,
|
||||
}, err
|
||||
}
|
||||
|
||||
type dhKEXGroup struct {
|
||||
size int
|
||||
p *big.Int
|
||||
}
|
||||
|
||||
// supportedDHKEXGroups returns the DH groups the server is willing to offer
|
||||
// for diffie-hellman-group-exchange-* key exchanges. The list is built lazily
|
||||
// on first use to keep the hex-to-big.Int parse out of package initialization.
|
||||
var supportedDHKEXGroups = sync.OnceValue(func() []dhKEXGroup {
|
||||
specs := []struct {
|
||||
size int
|
||||
hex string
|
||||
}{
|
||||
{2048, oakleyGroup14},
|
||||
{3072, oakleyGroup15},
|
||||
{4096, oakleyGroup16},
|
||||
}
|
||||
out := make([]dhKEXGroup, 0, len(specs))
|
||||
for _, s := range specs {
|
||||
p, _ := new(big.Int).SetString(s.hex, 16)
|
||||
out = append(out, dhKEXGroup{size: s.size, p: p})
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
// chooseDH picks a DH group for the given client request, mirroring the
|
||||
// algorithm used by OpenSSH's choose_dh in dh.c: prefer the smallest known
|
||||
// group larger than or equal to the client's PreferredBits, and otherwise pick
|
||||
// the largest group within the accepted [MinBits, MaxBits] range.
|
||||
func chooseDH(req kexDHGexRequestMsg) (*big.Int, error) {
|
||||
var best *big.Int
|
||||
bestSize := 0
|
||||
wantBits := int(req.PreferredBits)
|
||||
|
||||
for _, group := range supportedDHKEXGroups() {
|
||||
if uint32(group.size) < req.MinBits || uint32(group.size) > req.MaxBits {
|
||||
continue
|
||||
}
|
||||
|
||||
if bestSize == 0 {
|
||||
best = group.p
|
||||
bestSize = group.size
|
||||
continue
|
||||
}
|
||||
|
||||
closerFromAbove := group.size >= wantBits && group.size < bestSize
|
||||
closerFromBelow := group.size > bestSize && bestSize < wantBits
|
||||
|
||||
if closerFromAbove || closerFromBelow {
|
||||
best = group.p
|
||||
bestSize = group.size
|
||||
}
|
||||
}
|
||||
|
||||
if bestSize == 0 {
|
||||
return nil, fmt.Errorf("ssh: no suitable DH group found for request min: %d, preferred: %d, max: %d",
|
||||
req.MinBits, req.PreferredBits, req.MaxBits)
|
||||
}
|
||||
|
||||
return best, nil
|
||||
}
|
||||
|
||||
+96
-3
@@ -76,7 +76,7 @@ func parsePubKey(in []byte, algo string) (pubKey PublicKey, rest []byte, err err
|
||||
case InsecureKeyAlgoDSA:
|
||||
return parseDSA(in)
|
||||
case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521:
|
||||
return parseECDSA(in)
|
||||
return parseECDSA(in, algo)
|
||||
case KeyAlgoSKECDSA256:
|
||||
return parseSKECDSA(in)
|
||||
case KeyAlgoED25519:
|
||||
@@ -469,6 +469,12 @@ func parseRSA(in []byte) (out PublicKey, rest []byte, err error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 8192 bits is also the maximum RSA key size accepted by crypto/tls for
|
||||
// signature verification:
|
||||
// https://github.com/golang/go/blob/69801b25/src/crypto/tls/handshake_client.go#L1096
|
||||
if w.N.BitLen() > 8192 {
|
||||
return nil, nil, errors.New("ssh: rsa modulus too large")
|
||||
}
|
||||
if w.E.BitLen() > 24 {
|
||||
return nil, nil, errors.New("ssh: exponent too large")
|
||||
}
|
||||
@@ -574,6 +580,24 @@ func checkDSAParams(param *dsa.Parameters) error {
|
||||
return fmt.Errorf("ssh: unsupported DSA key size %d", l)
|
||||
}
|
||||
|
||||
// FIPS 186-2 specifies that Q must be exactly 160 bits. We must enforce
|
||||
// this to prevent DoS attacks where an attacker sends a huge Q which makes
|
||||
// verification slow.
|
||||
if l := param.Q.BitLen(); l != 160 {
|
||||
return fmt.Errorf("ssh: unsupported DSA sub-prime size %d", l)
|
||||
}
|
||||
|
||||
// The generator G is an element of the group, so it must be strictly less
|
||||
// than the modulus P.
|
||||
if param.G.Cmp(param.P) >= 0 {
|
||||
return errors.New("ssh: DSA generator larger than modulus")
|
||||
}
|
||||
|
||||
// G must be positive.
|
||||
if param.G.Sign() <= 0 {
|
||||
return errors.New("ssh: DSA generator must be positive")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -596,6 +620,14 @@ func parseDSA(in []byte) (out PublicKey, rest []byte, err error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// The public value Y must be a non-zero element of the group, i.e.
|
||||
// strictly between 0 and P. crypto/dsa.Verify does not range-check Y,
|
||||
// so we reject out-of-range values here to prevent a maliciously
|
||||
// oversized Y from slowing verification.
|
||||
if w.Y.Sign() <= 0 || w.Y.Cmp(w.P) >= 0 {
|
||||
return nil, nil, errors.New("ssh: DSA public value Y out of range")
|
||||
}
|
||||
|
||||
key := &dsaPublicKey{
|
||||
Parameters: param,
|
||||
Y: w.Y,
|
||||
@@ -774,7 +806,7 @@ func supportedEllipticCurve(curve elliptic.Curve) bool {
|
||||
}
|
||||
|
||||
// parseECDSA parses an ECDSA key according to RFC 5656, section 3.1.
|
||||
func parseECDSA(in []byte) (out PublicKey, rest []byte, err error) {
|
||||
func parseECDSA(in []byte, expectedType string) (out PublicKey, rest []byte, err error) {
|
||||
var w struct {
|
||||
Curve string
|
||||
KeyBytes []byte
|
||||
@@ -785,6 +817,12 @@ func parseECDSA(in []byte) (out PublicKey, rest []byte, err error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
actualType := "ecdsa-sha2-" + w.Curve
|
||||
if expectedType != actualType {
|
||||
return nil, nil, fmt.Errorf("ssh: algorithm type mismatch: expected %q, found curve %q (type %q)",
|
||||
expectedType, w.Curve, actualType)
|
||||
}
|
||||
|
||||
key := new(ecdsa.PublicKey)
|
||||
|
||||
switch w.Curve {
|
||||
@@ -869,11 +907,25 @@ type skFields struct {
|
||||
Counter uint32
|
||||
}
|
||||
|
||||
// flagUserPresence is the "user present" bit (UP) in the SK signature
|
||||
// flags, matching the FIDO CTAP2 authenticatorData UP flag. See
|
||||
// openssh/PROTOCOL.u2f.
|
||||
const flagUserPresence = 0x01
|
||||
|
||||
// errSKMissingUserPresence is returned by SK key Verify methods when
|
||||
// the signature does not assert user presence and the key was not
|
||||
// marked as no-touch-required.
|
||||
var errSKMissingUserPresence = errors.New("ssh: signature missing required user presence flag")
|
||||
|
||||
type skECDSAPublicKey struct {
|
||||
// application is a URL-like string, typically "ssh:" for SSH.
|
||||
// see openssh/PROTOCOL.u2f for details.
|
||||
application string
|
||||
ecdsa.PublicKey
|
||||
// noTouchRequired, when true, disables the default user-presence
|
||||
// check in Verify. It is set by skKeyWithoutUP on a clone of the
|
||||
// key, never on an instance shared across authentication attempts.
|
||||
noTouchRequired bool
|
||||
}
|
||||
|
||||
func (k *skECDSAPublicKey) Type() string {
|
||||
@@ -959,6 +1011,10 @@ func (k *skECDSAPublicKey) Verify(data []byte, sig *Signature) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if skf.Flags&flagUserPresence == 0 && !k.noTouchRequired {
|
||||
return errSKMissingUserPresence
|
||||
}
|
||||
|
||||
blob := struct {
|
||||
ApplicationDigest []byte `ssh:"rest"`
|
||||
Flags byte
|
||||
@@ -992,6 +1048,10 @@ type skEd25519PublicKey struct {
|
||||
// see openssh/PROTOCOL.u2f for details.
|
||||
application string
|
||||
ed25519.PublicKey
|
||||
// noTouchRequired, when true, disables the default user-presence
|
||||
// check in Verify. It is set by skKeyWithoutUP on a clone of the
|
||||
// key, never on an instance shared across authentication attempts.
|
||||
noTouchRequired bool
|
||||
}
|
||||
|
||||
func (k *skEd25519PublicKey) Type() string {
|
||||
@@ -1066,6 +1126,10 @@ func (k *skEd25519PublicKey) Verify(data []byte, sig *Signature) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if skf.Flags&flagUserPresence == 0 && !k.noTouchRequired {
|
||||
return errSKMissingUserPresence
|
||||
}
|
||||
|
||||
blob := struct {
|
||||
ApplicationDigest []byte `ssh:"rest"`
|
||||
Flags byte
|
||||
@@ -1408,6 +1472,17 @@ func passphraseProtectedOpenSSHKey(passphrase []byte) openSSHDecryptFunc {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// OpenSSH does not impose an upper bound on the bcrypt round count
|
||||
// stored in the key file, but bcrypt_pbkdf cost is linear in rounds:
|
||||
// the default is 16, ssh-keygen lets users pick anything up to
|
||||
// INT_MAX. Cap at 2048 (128x the default, a few seconds of CPU) so
|
||||
// that an oversized value in the file cannot tie up the caller for
|
||||
// months.
|
||||
const maxRounds = 1 << 11
|
||||
if opts.Rounds > maxRounds {
|
||||
return nil, fmt.Errorf("ssh: bcrypt KDF rounds %d exceed maximum %d", opts.Rounds, maxRounds)
|
||||
}
|
||||
|
||||
k, err := bcrypt_pbkdf.Key(passphrase, []byte(opts.Salt), int(opts.Rounds), 32+16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1577,10 +1652,28 @@ func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.Priv
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Mirror the validation done in parseRSA for public keys: cap the
|
||||
// modulus at the same limit enforced by crypto/tls, reject oversized
|
||||
// or invalid exponents, and additionally bound the prime factors to
|
||||
// avoid the expensive CRT coefficient recomputation in pk.Precompute.
|
||||
if key.N.BitLen() > 8192 {
|
||||
return nil, errors.New("ssh: rsa modulus too large")
|
||||
}
|
||||
if key.P.BitLen() > 4096 || key.Q.BitLen() > 4096 {
|
||||
return nil, errors.New("ssh: rsa prime too large")
|
||||
}
|
||||
if key.E.BitLen() > 24 {
|
||||
return nil, errors.New("ssh: exponent too large")
|
||||
}
|
||||
e := key.E.Int64()
|
||||
if e < 3 || e&1 == 0 {
|
||||
return nil, errors.New("ssh: incorrect exponent")
|
||||
}
|
||||
|
||||
pk := &rsa.PrivateKey{
|
||||
PublicKey: rsa.PublicKey{
|
||||
N: key.N,
|
||||
E: int(key.E.Int64()),
|
||||
E: int(e),
|
||||
},
|
||||
D: key.D,
|
||||
Primes: []*big.Int{key.P, key.Q},
|
||||
|
||||
+27
-6
@@ -160,8 +160,13 @@ func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool
|
||||
|
||||
// IsRevoked can be used as a callback in ssh.CertChecker
|
||||
func (db *hostKeyDB) IsRevoked(key *ssh.Certificate) bool {
|
||||
_, ok := db.revoked[string(key.Marshal())]
|
||||
return ok
|
||||
if _, ok := db.revoked[string(key.Marshal())]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := db.revoked[string(key.SignatureKey.Marshal())]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const markerCert = "@cert-authority"
|
||||
@@ -173,7 +178,7 @@ func nextWord(line []byte) (string, []byte) {
|
||||
return string(line), nil
|
||||
}
|
||||
|
||||
return string(line[:i]), bytes.TrimSpace(line[i:])
|
||||
return string(line[:i]), trimSpace(line[i:])
|
||||
}
|
||||
|
||||
func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error) {
|
||||
@@ -183,12 +188,17 @@ func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error)
|
||||
}
|
||||
|
||||
host, line = nextWord(line)
|
||||
// If the extracted 'host' starts with '@', it means we either encountered
|
||||
// a second marker (e.g., "@cert-authority @revoked") or an unknown marker
|
||||
// (e.g., "@unknown"). Both are invalid.
|
||||
if len(host) > 0 && host[0] == '@' {
|
||||
return "", "", nil, fmt.Errorf("knownhosts: unexpected marker: %q", host)
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return "", "", nil, errors.New("knownhosts: missing host pattern")
|
||||
}
|
||||
|
||||
// ignore the keytype as it's in the key blob anyway.
|
||||
_, line = nextWord(line)
|
||||
wantType, line := nextWord(line)
|
||||
if len(line) == 0 {
|
||||
return "", "", nil, errors.New("knownhosts: missing key type pattern")
|
||||
}
|
||||
@@ -204,6 +214,10 @@ func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error)
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
if key.Type() != wantType {
|
||||
return "", "", nil, fmt.Errorf("knownhosts: key type mismatch: found %q, want %q", key.Type(), wantType)
|
||||
}
|
||||
|
||||
return marker, host, key, nil
|
||||
}
|
||||
|
||||
@@ -382,7 +396,7 @@ func (db *hostKeyDB) Read(r io.Reader, filename string) error {
|
||||
for scanner.Scan() {
|
||||
lineNum++
|
||||
line := scanner.Bytes()
|
||||
line = bytes.TrimSpace(line)
|
||||
line = trimSpace(line)
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
@@ -530,3 +544,10 @@ func newHashedHost(encoded string) (*hashedHost, error) {
|
||||
func (h *hashedHost) match(a addr) bool {
|
||||
return bytes.Equal(hashHost(Normalize(a.String()), h.salt), h.hash)
|
||||
}
|
||||
|
||||
// trimSpace removes leading and trailing ASCII whitespace (space and tab). It
|
||||
// is used instead of bytes.TrimSpace to match OpenSSH behavior, which strictly
|
||||
// parses only ASCII space (0x20) and tab (0x09) as whitespace.
|
||||
func trimSpace(in []byte) []byte {
|
||||
return bytes.Trim(in, " \t")
|
||||
}
|
||||
|
||||
+35
-4
@@ -91,9 +91,10 @@ type mux struct {
|
||||
|
||||
incomingChannels chan NewChannel
|
||||
|
||||
globalSentMu sync.Mutex
|
||||
globalResponses chan interface{}
|
||||
incomingRequests chan *Request
|
||||
globalSentMu sync.Mutex
|
||||
globalSentPending atomic.Bool
|
||||
globalResponses chan interface{}
|
||||
incomingRequests chan *Request
|
||||
|
||||
errCond *sync.Cond
|
||||
err error
|
||||
@@ -141,6 +142,27 @@ func (m *mux) SendRequest(name string, wantReply bool, payload []byte) (bool, []
|
||||
if wantReply {
|
||||
m.globalSentMu.Lock()
|
||||
defer m.globalSentMu.Unlock()
|
||||
|
||||
// Open the gate so that responses arriving while this request is in
|
||||
// flight are allowed to reach globalResponses. Any response arriving
|
||||
// while no request is pending is dropped by handleGlobalPacket.
|
||||
m.globalSentPending.Store(true)
|
||||
defer m.globalSentPending.Store(false)
|
||||
|
||||
// Drain any spurious responses that may have been buffered. This prevents
|
||||
// a previously buffered unexpected response from being consumed instead
|
||||
// of the actual response for this request.
|
||||
drain:
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-m.globalResponses:
|
||||
if !ok {
|
||||
break drain
|
||||
}
|
||||
default:
|
||||
break drain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.sendMessage(globalRequestMsg{
|
||||
@@ -267,7 +289,16 @@ func (m *mux) handleGlobalPacket(packet []byte) error {
|
||||
mux: m,
|
||||
}
|
||||
case *globalRequestSuccessMsg, *globalRequestFailureMsg:
|
||||
m.globalResponses <- msg
|
||||
// Drop responses that arrive when no SendRequest is waiting, to
|
||||
// prevent a malicious peer from staging responses for a future
|
||||
// caller.
|
||||
if !m.globalSentPending.Load() {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case m.globalResponses <- msg:
|
||||
default:
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("not a global message %#v", msg))
|
||||
}
|
||||
|
||||
+147
-18
@@ -34,21 +34,29 @@ type Permissions struct {
|
||||
// or not supported.
|
||||
CriticalOptions map[string]string
|
||||
|
||||
// Extensions are extra functionality that the server may
|
||||
// offer on authenticated connections. Lack of support for an
|
||||
// extension does not preclude authenticating a user. Common
|
||||
// extensions are "permit-agent-forwarding",
|
||||
// "permit-X11-forwarding". The Go SSH library currently does
|
||||
// not act on any extension, and it is up to server
|
||||
// implementations to honor them. Extensions can be used to
|
||||
// pass data from the authentication callbacks to the server
|
||||
// application layer.
|
||||
// Extensions are extra functionality that the server may offer on
|
||||
// authenticated connections. Lack of support for an extension does not
|
||||
// preclude authenticating a user. Common extensions are
|
||||
// "permit-agent-forwarding", "permit-X11-forwarding". In general the Go
|
||||
// SSH library does not act on extensions and it is up to server
|
||||
// implementations to honor them; extensions can also be used to pass data
|
||||
// from the authentication callbacks to the server application layer.
|
||||
//
|
||||
// The one extension acted upon by this library is "no-touch-required",
|
||||
// which applies only to security-key public keys
|
||||
// (sk-ecdsa-sha2-nistp256@openssh.com and sk-ssh-ed25519@openssh.com).
|
||||
// When present, it waives the default requirement that SK signatures
|
||||
// assert user presence (i.e. a physical touch of the authenticator)
|
||||
// during signature verification.
|
||||
Extensions map[string]string
|
||||
|
||||
// ExtraData allows to store user defined data.
|
||||
ExtraData map[any]any
|
||||
}
|
||||
|
||||
// GSSAPIWithMICConfig includes the server callbacks for gssapi-with-mic
|
||||
// authentication. If either field is nil, gssapi-with-mic is considered not
|
||||
// configured.
|
||||
type GSSAPIWithMICConfig struct {
|
||||
// AllowLogin, must be set, is called when gssapi-with-mic
|
||||
// authentication is selected (RFC 4462 section 3). The srcName is from the
|
||||
@@ -63,6 +71,10 @@ type GSSAPIWithMICConfig struct {
|
||||
Server GSSAPIServer
|
||||
}
|
||||
|
||||
func gssapiWithMICConfigured(config *GSSAPIWithMICConfig) bool {
|
||||
return config != nil && config.AllowLogin != nil && config.Server != nil
|
||||
}
|
||||
|
||||
// SendAuthBanner implements [ServerPreAuthConn].
|
||||
func (s *connection) SendAuthBanner(msg string) error {
|
||||
return s.transport.writePacket(Marshal(&userAuthBannerMsg{
|
||||
@@ -84,6 +96,79 @@ type ServerPreAuthConn interface {
|
||||
SendAuthBanner(string) error
|
||||
}
|
||||
|
||||
// noTouchRequiredExtension is the extension name used by OpenSSH in
|
||||
// authorized_keys options and certificate extensions to mark keys
|
||||
// whose signatures do not need to assert user presence (touch). See
|
||||
// ssh-keygen(1) and sshd(8).
|
||||
const noTouchRequiredExtension = "no-touch-required"
|
||||
|
||||
// noTouchAllowed reports whether the user presence requirement on
|
||||
// SK signatures should be waived for this authentication attempt. The
|
||||
// requirement is waived when the "no-touch-required" extension is
|
||||
// present either in the Permissions returned by the auth callback
|
||||
// (authorized_keys-level opt-out) or in the certificate's own
|
||||
// Extensions (CA-level opt-out), matching OpenSSH behavior. OpenSSH
|
||||
// reads the per-key opt-out only from cert Extensions and
|
||||
// authorized_keys options (never from CriticalOptions); we follow the
|
||||
// same rule.
|
||||
func noTouchAllowed(pubKey PublicKey, perms *Permissions) bool {
|
||||
if perms != nil {
|
||||
if _, ok := perms.Extensions[noTouchRequiredExtension]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if cert, ok := pubKey.(*Certificate); ok {
|
||||
if _, ok := cert.Extensions[noTouchRequiredExtension]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// skKeyWithoutUP returns a PublicKey equivalent to pubKey but whose
|
||||
// Verify accepts SK signatures with the user-presence flag clear. If
|
||||
// pubKey is not (and does not wrap) an SK key, pubKey is returned
|
||||
// unchanged. The returned value never mutates pubKey: for SK keys a
|
||||
// shallow copy is made so that the noTouchRequired flag is set only on
|
||||
// the clone.
|
||||
//
|
||||
// The implementation is iterative rather than recursive. When pubKey
|
||||
// is a *Certificate we unwrap exactly one level to look at the inner
|
||||
// key. The SSH cert format forbids Certificate.Key from being another
|
||||
// Certificate (parseCert rejects it), but nothing stops callers from
|
||||
// constructing such a value directly in Go; a recursive descent could
|
||||
// otherwise be driven to unbounded depth by a hand-crafted or cyclic
|
||||
// Certificate. A malformed input of that shape simply returns
|
||||
// unchanged here.
|
||||
func skKeyWithoutUP(pubKey PublicKey) PublicKey {
|
||||
cert, isCert := pubKey.(*Certificate)
|
||||
target := pubKey
|
||||
if isCert {
|
||||
target = cert.Key
|
||||
}
|
||||
var cloned PublicKey
|
||||
switch k := target.(type) {
|
||||
case *skECDSAPublicKey:
|
||||
c := *k
|
||||
c.noTouchRequired = true
|
||||
cloned = &c
|
||||
case *skEd25519PublicKey:
|
||||
c := *k
|
||||
c.noTouchRequired = true
|
||||
cloned = &c
|
||||
default:
|
||||
// Not an SK key (or a pathological *Certificate wrapping
|
||||
// another *Certificate): pubKey is already usable for Verify.
|
||||
return pubKey
|
||||
}
|
||||
if !isCert {
|
||||
return cloned
|
||||
}
|
||||
c := *cert
|
||||
c.Key = cloned
|
||||
return &c
|
||||
}
|
||||
|
||||
// ServerConfig holds server specific configuration data.
|
||||
type ServerConfig struct {
|
||||
// Config contains configuration shared between client and server.
|
||||
@@ -242,8 +327,10 @@ func (c *pubKeyCache) add(candidate cachedPubKey) {
|
||||
type ServerConn struct {
|
||||
Conn
|
||||
|
||||
// If the succeeding authentication callback returned a
|
||||
// non-nil Permissions pointer, it is stored here.
|
||||
// If the succeeding authentication callback returned a non-nil Permissions
|
||||
// pointer, it is stored here. These are the permissions from the final,
|
||||
// successful authentication method. Permissions returned by callbacks that
|
||||
// return PartialSuccessError are not preserved and must be nil.
|
||||
Permissions *Permissions
|
||||
}
|
||||
|
||||
@@ -302,8 +389,7 @@ func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error)
|
||||
}
|
||||
|
||||
if !config.NoClientAuth && config.PasswordCallback == nil && config.PublicKeyCallback == nil &&
|
||||
config.KeyboardInteractiveCallback == nil && (config.GSSAPIWithMICConfig == nil ||
|
||||
config.GSSAPIWithMICConfig.AllowLogin == nil || config.GSSAPIWithMICConfig.Server == nil) {
|
||||
config.KeyboardInteractiveCallback == nil && !gssapiWithMICConfigured(config.GSSAPIWithMICConfig) {
|
||||
return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
|
||||
}
|
||||
|
||||
@@ -527,6 +613,15 @@ func (b *BannerError) Error() string {
|
||||
return b.Err.Error()
|
||||
}
|
||||
|
||||
// maxAuthServerAttempts caps the total number of SSH_MSG_USERAUTH_REQUEST
|
||||
// messages the server will process on a single connection, regardless of
|
||||
// outcome (failure, partial success, public key query, or none). It is a
|
||||
// backstop against clients that drive the authentication loop indefinitely
|
||||
// without ever incurring a real failure — for example by repeatedly
|
||||
// triggering PartialSuccessError or by spamming public key offer queries —
|
||||
// neither of which increment the MaxAuthTries failure counter.
|
||||
const maxAuthServerAttempts = 128
|
||||
|
||||
func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, error) {
|
||||
if config.PreAuthConnCallback != nil {
|
||||
config.PreAuthConnCallback(s)
|
||||
@@ -537,6 +632,7 @@ func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, err
|
||||
var perms *Permissions
|
||||
|
||||
authFailures := 0
|
||||
authAttempts := 0
|
||||
noneAuthCount := 0
|
||||
var authErrs []error
|
||||
var calledBannerCallback bool
|
||||
@@ -565,6 +661,19 @@ userAuthLoop:
|
||||
return nil, &ServerAuthError{Errors: authErrs}
|
||||
}
|
||||
|
||||
if authAttempts >= maxAuthServerAttempts {
|
||||
discMsg := &disconnectMsg{
|
||||
Reason: 2,
|
||||
Message: "too many authentication attempts",
|
||||
}
|
||||
if err := s.transport.writePacket(Marshal(discMsg)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authErrs = append(authErrs, discMsg)
|
||||
return nil, &ServerAuthError{Errors: authErrs}
|
||||
}
|
||||
authAttempts++
|
||||
|
||||
var userAuthReq userAuthRequestMsg
|
||||
if packet, err := s.transport.readPacket(); err != nil {
|
||||
if err == io.EOF {
|
||||
@@ -737,8 +846,15 @@ userAuthLoop:
|
||||
}
|
||||
|
||||
signedData := buildDataSignedForAuth(sessionID, userAuthReq, algo, pubKeyData)
|
||||
|
||||
if err := pubKey.Verify(signedData, sig); err != nil {
|
||||
// pubKey is reused below for VerifiedPublicKeyCallback and
|
||||
// must remain the key as presented by the client; derive a
|
||||
// separate value for Verify that carries any applicable
|
||||
// no-touch-required opt-out.
|
||||
pubKeyForVerify := pubKey
|
||||
if noTouchAllowed(pubKey, candidate.perms) {
|
||||
pubKeyForVerify = skKeyWithoutUP(pubKey)
|
||||
}
|
||||
if err := pubKeyForVerify.Verify(signedData, sig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -750,9 +866,16 @@ userAuthLoop:
|
||||
// considered verified and the callback must not run.
|
||||
perms, authErr = config.VerifiedPublicKeyCallback(s, pubKey, perms, algo)
|
||||
}
|
||||
if authErr == nil && perms != nil && perms.CriticalOptions != nil {
|
||||
if saco := perms.CriticalOptions[sourceAddressCriticalOption]; saco != "" {
|
||||
if err := checkSourceAddress(s.RemoteAddr(), saco); err != nil {
|
||||
authErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case "gssapi-with-mic":
|
||||
if authConfig.GSSAPIWithMICConfig == nil {
|
||||
if !gssapiWithMICConfigured(authConfig.GSSAPIWithMICConfig) {
|
||||
authErr = errors.New("ssh: gssapi-with-mic auth not configured")
|
||||
break
|
||||
}
|
||||
@@ -824,6 +947,13 @@ userAuthLoop:
|
||||
var failureMsg userAuthFailureMsg
|
||||
|
||||
if partialSuccess, ok := authErr.(*PartialSuccessError); ok {
|
||||
// Permissions are not preserved between authentication steps. To
|
||||
// avoid confusion about the final state of the connection, we
|
||||
// disallow returning non-nil Permissions combined with
|
||||
// PartialSuccessError.
|
||||
if perms != nil {
|
||||
return nil, errors.New("ssh: permissions must be nil when returning PartialSuccessError")
|
||||
}
|
||||
// After a partial success error we don't allow changing the user
|
||||
// name and execute the NoClientAuthCallback.
|
||||
partialSuccessReturned = true
|
||||
@@ -878,8 +1008,7 @@ userAuthLoop:
|
||||
if authConfig.KeyboardInteractiveCallback != nil {
|
||||
failureMsg.Methods = append(failureMsg.Methods, "keyboard-interactive")
|
||||
}
|
||||
if authConfig.GSSAPIWithMICConfig != nil && authConfig.GSSAPIWithMICConfig.Server != nil &&
|
||||
authConfig.GSSAPIWithMICConfig.AllowLogin != nil {
|
||||
if gssapiWithMICConfigured(authConfig.GSSAPIWithMICConfig) {
|
||||
failureMsg.Methods = append(failureMsg.Methods, "gssapi-with-mic")
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -423,6 +423,9 @@ func (s *Session) wait(reqs <-chan *Request) error {
|
||||
for msg := range reqs {
|
||||
switch msg.Type {
|
||||
case "exit-status":
|
||||
if len(msg.Payload) < 4 {
|
||||
return errors.New("ssh: malformed exit-status request")
|
||||
}
|
||||
wm.status = int(binary.BigEndian.Uint32(msg.Payload))
|
||||
case "exit-signal":
|
||||
var sigval struct {
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
This package (golang.org/x/net/http2) is the original source of truth
|
||||
of the Go HTTP/2 implementation.
|
||||
|
||||
As of Go 1.27, the source of truth has moved to the standard library
|
||||
package net/http/internal/http2.
|
||||
All new feature development should happen in that package.
|
||||
Only critical bug fixes and security fixes will be backported to x/net.
|
||||
|
||||
The x/net package contains two implementations of the HTTP/2 transport and server:
|
||||
|
||||
The original implementation (no longer the source of truth).
|
||||
|
||||
A reimplementation of the x/net/http2 APIs in terms of net/http.
|
||||
This is called "the wrapping implementation", since it wraps net/http.
|
||||
|
||||
The original implementation is used when the Go version is less than 1.27.
|
||||
|
||||
The wrapping implementation is used when the Go version is at least 1.27.
|
||||
The build tag "http2legacy" may be set to use the original implementation.
|
||||
+2
-12
@@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !(go1.27 && !http2legacy)
|
||||
|
||||
// Transport code's client connection pooling.
|
||||
|
||||
package http2
|
||||
@@ -14,18 +16,6 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ClientConnPool manages a pool of HTTP/2 client connections.
|
||||
type ClientConnPool interface {
|
||||
// GetClientConn returns a specific HTTP/2 connection (usually
|
||||
// a TLS-TCP connection) to an HTTP/2 server. On success, the
|
||||
// returned ClientConn accounts for the upcoming RoundTrip
|
||||
// call, so the caller should not omit it. If the caller needs
|
||||
// to, ClientConn.RoundTrip can be called with a bogus
|
||||
// new(http.Request) to release the stream reservation.
|
||||
GetClientConn(req *http.Request, addr string) (*ClientConn, error)
|
||||
MarkDead(*ClientConn)
|
||||
}
|
||||
|
||||
// clientConnPoolIdleCloser is the interface implemented by ClientConnPool
|
||||
// implementations which can close their idle connections.
|
||||
type clientConnPoolIdleCloser interface {
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package http2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return cc.roundTrip(req)
|
||||
}
|
||||
|
||||
// SetDoNotReuse marks cc as not reusable for future HTTP requests.
|
||||
func (cc *ClientConn) SetDoNotReuse() {
|
||||
cc.setDoNotReuse()
|
||||
}
|
||||
|
||||
// CanTakeNewRequest reports whether the connection can take a new request,
|
||||
// meaning it has not been closed or received or sent a GOAWAY.
|
||||
//
|
||||
// If the caller is going to immediately make a new request on this
|
||||
// connection, use ReserveNewRequest instead.
|
||||
func (cc *ClientConn) CanTakeNewRequest() bool {
|
||||
return cc.canTakeNewRequest()
|
||||
}
|
||||
|
||||
// ReserveNewRequest is like CanTakeNewRequest but also reserves a
|
||||
// concurrent stream in cc. The reservation is decremented on the
|
||||
// next call to RoundTrip.
|
||||
func (cc *ClientConn) ReserveNewRequest() bool {
|
||||
return cc.reserveNewRequest()
|
||||
}
|
||||
|
||||
// State returns a snapshot of cc's state.
|
||||
func (cc *ClientConn) State() ClientConnState {
|
||||
return cc.state()
|
||||
}
|
||||
|
||||
// Shutdown gracefully closes the client connection, waiting for running streams to complete.
|
||||
func (cc *ClientConn) Shutdown(ctx context.Context) error {
|
||||
return cc.shutdown(ctx)
|
||||
}
|
||||
|
||||
// Close closes the client connection immediately.
|
||||
//
|
||||
// In-flight requests are interrupted. For a graceful shutdown, use Shutdown instead.
|
||||
func (cc *ClientConn) Close() error {
|
||||
return cc.close()
|
||||
}
|
||||
|
||||
// Ping sends a PING frame to the server and waits for the ack.
|
||||
func (cc *ClientConn) Ping(ctx context.Context) error {
|
||||
return cc.ping(ctx)
|
||||
}
|
||||
+2
@@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !(go1.27 && !http2legacy)
|
||||
|
||||
package http2
|
||||
|
||||
import (
|
||||
|
||||
+3
-10
@@ -6,7 +6,6 @@ package hpack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// headerFieldTable implements a list of HeaderFields.
|
||||
@@ -55,16 +54,10 @@ func (t *headerFieldTable) len() int {
|
||||
|
||||
// addEntry adds a new entry.
|
||||
func (t *headerFieldTable) addEntry(f HeaderField) {
|
||||
// Prevent f from escaping to the heap.
|
||||
f2 := HeaderField{
|
||||
Name: strings.Clone(f.Name),
|
||||
Value: strings.Clone(f.Value),
|
||||
Sensitive: f.Sensitive,
|
||||
}
|
||||
id := uint64(t.len()) + t.evictCount + 1
|
||||
t.byName[f2.Name] = id
|
||||
t.byNameValue[pairNameValue{f2.Name, f2.Value}] = id
|
||||
t.ents = append(t.ents, f2)
|
||||
t.byName[f.Name] = id
|
||||
t.byNameValue[pairNameValue{f.Name, f.Value}] = id
|
||||
t.ents = append(t.ents, f)
|
||||
}
|
||||
|
||||
// evictOldest evicts the n oldest entries in the table.
|
||||
|
||||
+1
-1
@@ -195,7 +195,7 @@ func (s SettingID) String() string {
|
||||
}
|
||||
|
||||
// validWireHeaderFieldName reports whether v is a valid header field
|
||||
// name (key). See httpguts.ValidHeaderName for the base rules.
|
||||
// name (key). See httpguts.ValidHeaderFieldName for the base rules.
|
||||
//
|
||||
// Further, http2 says:
|
||||
//
|
||||
|
||||
+9
-205
@@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !(go1.27 && !http2legacy)
|
||||
|
||||
// TODO: turn off the serve goroutine when idle, so
|
||||
// an idle conn only has the readFrames goroutine active. (which could
|
||||
// also be optimized probably to pin less memory in crypto/tls). This
|
||||
@@ -88,98 +90,6 @@ var (
|
||||
testHookOnPanic func(sc *serverConn, panicVal interface{}) (rePanic bool)
|
||||
)
|
||||
|
||||
// Server is an HTTP/2 server.
|
||||
type Server struct {
|
||||
// MaxHandlers limits the number of http.Handler ServeHTTP goroutines
|
||||
// which may run at a time over all connections.
|
||||
// Negative or zero no limit.
|
||||
// TODO: implement
|
||||
MaxHandlers int
|
||||
|
||||
// MaxConcurrentStreams optionally specifies the number of
|
||||
// concurrent streams that each client may have open at a
|
||||
// time. This is unrelated to the number of http.Handler goroutines
|
||||
// which may be active globally, which is MaxHandlers.
|
||||
// If zero, MaxConcurrentStreams defaults to at least 100, per
|
||||
// the HTTP/2 spec's recommendations.
|
||||
MaxConcurrentStreams uint32
|
||||
|
||||
// MaxDecoderHeaderTableSize optionally specifies the http2
|
||||
// SETTINGS_HEADER_TABLE_SIZE to send in the initial settings frame. It
|
||||
// informs the remote endpoint of the maximum size of the header compression
|
||||
// table used to decode header blocks, in octets. If zero, the default value
|
||||
// of 4096 is used.
|
||||
MaxDecoderHeaderTableSize uint32
|
||||
|
||||
// MaxEncoderHeaderTableSize optionally specifies an upper limit for the
|
||||
// header compression table used for encoding request headers. Received
|
||||
// SETTINGS_HEADER_TABLE_SIZE settings are capped at this limit. If zero,
|
||||
// the default value of 4096 is used.
|
||||
MaxEncoderHeaderTableSize uint32
|
||||
|
||||
// MaxReadFrameSize optionally specifies the largest frame
|
||||
// this server is willing to read. A valid value is between
|
||||
// 16k and 16M, inclusive. If zero or otherwise invalid, a
|
||||
// default value is used.
|
||||
MaxReadFrameSize uint32
|
||||
|
||||
// PermitProhibitedCipherSuites, if true, permits the use of
|
||||
// cipher suites prohibited by the HTTP/2 spec.
|
||||
PermitProhibitedCipherSuites bool
|
||||
|
||||
// IdleTimeout specifies how long until idle clients should be
|
||||
// closed with a GOAWAY frame. PING frames are not considered
|
||||
// activity for the purposes of IdleTimeout.
|
||||
// If zero or negative, there is no timeout.
|
||||
IdleTimeout time.Duration
|
||||
|
||||
// ReadIdleTimeout is the timeout after which a health check using a ping
|
||||
// frame will be carried out if no frame is received on the connection.
|
||||
// If zero, no health check is performed.
|
||||
ReadIdleTimeout time.Duration
|
||||
|
||||
// PingTimeout is the timeout after which the connection will be closed
|
||||
// if a response to a ping is not received.
|
||||
// If zero, a default of 15 seconds is used.
|
||||
PingTimeout time.Duration
|
||||
|
||||
// WriteByteTimeout is the timeout after which a connection will be
|
||||
// closed if no data can be written to it. The timeout begins when data is
|
||||
// available to write, and is extended whenever any bytes are written.
|
||||
// If zero or negative, there is no timeout.
|
||||
WriteByteTimeout time.Duration
|
||||
|
||||
// MaxUploadBufferPerConnection is the size of the initial flow
|
||||
// control window for each connections. The HTTP/2 spec does not
|
||||
// allow this to be smaller than 65535 or larger than 2^32-1.
|
||||
// If the value is outside this range, a default value will be
|
||||
// used instead.
|
||||
MaxUploadBufferPerConnection int32
|
||||
|
||||
// MaxUploadBufferPerStream is the size of the initial flow control
|
||||
// window for each stream. The HTTP/2 spec does not allow this to
|
||||
// be larger than 2^32-1. If the value is zero or larger than the
|
||||
// maximum, a default value will be used instead.
|
||||
MaxUploadBufferPerStream int32
|
||||
|
||||
// NewWriteScheduler constructs a write scheduler for a connection.
|
||||
// If nil, a default scheduler is chosen.
|
||||
//
|
||||
// Deprecated: User-provided write schedulers are deprecated.
|
||||
NewWriteScheduler func() WriteScheduler
|
||||
|
||||
// CountError, if non-nil, is called on HTTP/2 server errors.
|
||||
// It's intended to increment a metric for monitoring, such
|
||||
// as an expvar or Prometheus metric.
|
||||
// The errType consists of only ASCII word characters.
|
||||
CountError func(errType string)
|
||||
|
||||
// Internal state. This is a pointer (rather than embedded directly)
|
||||
// so that we don't embed a Mutex in this struct, which will make the
|
||||
// struct non-copyable, which might break some callers.
|
||||
state *serverInternalState
|
||||
}
|
||||
|
||||
type serverInternalState struct {
|
||||
mu sync.Mutex
|
||||
activeConns map[*serverConn]struct{}
|
||||
@@ -187,6 +97,9 @@ type serverInternalState struct {
|
||||
// Pool of error channels. This is per-Server rather than global
|
||||
// because channels can't be reused across synctest bubbles.
|
||||
errChanPool sync.Pool
|
||||
|
||||
// Used in tests.
|
||||
testNewConn func(*serverConn)
|
||||
}
|
||||
|
||||
func (s *serverInternalState) registerConn(sc *serverConn) {
|
||||
@@ -239,12 +152,7 @@ func (s *serverInternalState) putErrChan(ch chan error) {
|
||||
s.errChanPool.Put(ch)
|
||||
}
|
||||
|
||||
// ConfigureServer adds HTTP/2 support to a net/http Server.
|
||||
//
|
||||
// The configuration conf may be nil.
|
||||
//
|
||||
// ConfigureServer must be called before s begins serving.
|
||||
func ConfigureServer(s *http.Server, conf *Server) error {
|
||||
func configureServer(s *http.Server, conf *Server) error {
|
||||
if s == nil {
|
||||
panic("nil *http.Server")
|
||||
}
|
||||
@@ -349,83 +257,6 @@ func ConfigureServer(s *http.Server, conf *Server) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeConnOpts are options for the Server.ServeConn method.
|
||||
type ServeConnOpts struct {
|
||||
// Context is the base context to use.
|
||||
// If nil, context.Background is used.
|
||||
Context context.Context
|
||||
|
||||
// BaseConfig optionally sets the base configuration
|
||||
// for values. If nil, defaults are used.
|
||||
BaseConfig *http.Server
|
||||
|
||||
// Handler specifies which handler to use for processing
|
||||
// requests. If nil, BaseConfig.Handler is used. If BaseConfig
|
||||
// or BaseConfig.Handler is nil, http.DefaultServeMux is used.
|
||||
Handler http.Handler
|
||||
|
||||
// UpgradeRequest is an initial request received on a connection
|
||||
// undergoing an h2c upgrade. The request body must have been
|
||||
// completely read from the connection before calling ServeConn,
|
||||
// and the 101 Switching Protocols response written.
|
||||
UpgradeRequest *http.Request
|
||||
|
||||
// Settings is the decoded contents of the HTTP2-Settings header
|
||||
// in an h2c upgrade request.
|
||||
Settings []byte
|
||||
|
||||
// SawClientPreface is set if the HTTP/2 connection preface
|
||||
// has already been read from the connection.
|
||||
SawClientPreface bool
|
||||
}
|
||||
|
||||
func (o *ServeConnOpts) context() context.Context {
|
||||
if o != nil && o.Context != nil {
|
||||
return o.Context
|
||||
}
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
func (o *ServeConnOpts) baseConfig() *http.Server {
|
||||
if o != nil && o.BaseConfig != nil {
|
||||
return o.BaseConfig
|
||||
}
|
||||
return new(http.Server)
|
||||
}
|
||||
|
||||
func (o *ServeConnOpts) handler() http.Handler {
|
||||
if o != nil {
|
||||
if o.Handler != nil {
|
||||
return o.Handler
|
||||
}
|
||||
if o.BaseConfig != nil && o.BaseConfig.Handler != nil {
|
||||
return o.BaseConfig.Handler
|
||||
}
|
||||
}
|
||||
return http.DefaultServeMux
|
||||
}
|
||||
|
||||
// ServeConn serves HTTP/2 requests on the provided connection and
|
||||
// blocks until the connection is no longer readable.
|
||||
//
|
||||
// ServeConn starts speaking HTTP/2 assuming that c has not had any
|
||||
// reads or writes. It writes its initial settings frame and expects
|
||||
// to be able to read the preface and settings frame from the
|
||||
// client. If c has a ConnectionState method like a *tls.Conn, the
|
||||
// ConnectionState is used to verify the TLS ciphersuite and to set
|
||||
// the Request.TLS field in Handlers.
|
||||
//
|
||||
// ServeConn does not support h2c by itself. Any h2c support must be
|
||||
// implemented in terms of providing a suitably-behaving net.Conn.
|
||||
//
|
||||
// The opts parameter is optional. If nil, default values are used.
|
||||
func (s *Server) ServeConn(c net.Conn, opts *ServeConnOpts) {
|
||||
if opts == nil {
|
||||
opts = &ServeConnOpts{}
|
||||
}
|
||||
s.serveConn(c, opts, nil)
|
||||
}
|
||||
|
||||
func (s *Server) serveConn(c net.Conn, opts *ServeConnOpts, newf func(*serverConn)) {
|
||||
baseCtx, cancel := serverConnBaseContext(c, opts)
|
||||
defer cancel()
|
||||
@@ -461,6 +292,9 @@ func (s *Server) serveConn(c net.Conn, opts *ServeConnOpts, newf func(*serverCon
|
||||
if newf != nil {
|
||||
newf(sc)
|
||||
}
|
||||
if s.state != nil && s.state.testNewConn != nil {
|
||||
s.state.testNewConn(sc)
|
||||
}
|
||||
|
||||
s.state.registerConn(sc)
|
||||
defer s.state.unregisterConn(sc)
|
||||
@@ -570,15 +404,6 @@ func (s *Server) serveConn(c net.Conn, opts *ServeConnOpts, newf func(*serverCon
|
||||
sc.serve(conf)
|
||||
}
|
||||
|
||||
func serverConnBaseContext(c net.Conn, opts *ServeConnOpts) (ctx context.Context, cancel func()) {
|
||||
ctx, cancel = context.WithCancel(opts.context())
|
||||
ctx = context.WithValue(ctx, http.LocalAddrContextKey, c.LocalAddr())
|
||||
if hs := opts.baseConfig(); hs != nil {
|
||||
ctx = context.WithValue(ctx, http.ServerContextKey, hs)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (sc *serverConn) rejectConn(err ErrCode, debug string) {
|
||||
sc.vlogf("http2: server rejecting conn: %v, %s", err, debug)
|
||||
// ignoring errors. hanging up anyway.
|
||||
@@ -2832,21 +2657,6 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// TrailerPrefix is a magic prefix for ResponseWriter.Header map keys
|
||||
// that, if present, signals that the map entry is actually for
|
||||
// the response trailers, and not the response headers. The prefix
|
||||
// is stripped after the ServeHTTP call finishes and the values are
|
||||
// sent in the trailers.
|
||||
//
|
||||
// This mechanism is intended only for trailers that are not known
|
||||
// prior to the headers being written. If the set of trailers is fixed
|
||||
// or known before the header is written, the normal Go trailers mechanism
|
||||
// is preferred:
|
||||
//
|
||||
// https://golang.org/pkg/net/http/#ResponseWriter
|
||||
// https://golang.org/pkg/net/http/#example_ResponseWriter_trailers
|
||||
const TrailerPrefix = "Trailer:"
|
||||
|
||||
// promoteUndeclaredTrailers permits http.Handlers to set trailers
|
||||
// after the header has already been flushed. Because the Go
|
||||
// ResponseWriter interface has no way to set Trailers (only the
|
||||
@@ -3123,12 +2933,6 @@ func (w *responseWriter) handlerDone() {
|
||||
responseWriterStatePool.Put(rws)
|
||||
}
|
||||
|
||||
// Push errors.
|
||||
var (
|
||||
ErrRecursivePush = errors.New("http2: recursive push not allowed")
|
||||
ErrPushLimitReached = errors.New("http2: push would exceed peer's SETTINGS_MAX_CONCURRENT_STREAMS")
|
||||
)
|
||||
|
||||
var _ http.Pusher = (*responseWriter)(nil)
|
||||
|
||||
func (w *responseWriter) Push(target string, opts *http.PushOptions) error {
|
||||
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
// Copyright 2026 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package http2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TrailerPrefix is a magic prefix for ResponseWriter.Header map keys
|
||||
// that, if present, signals that the map entry is actually for
|
||||
// the response trailers, and not the response headers. The prefix
|
||||
// is stripped after the ServeHTTP call finishes and the values are
|
||||
// sent in the trailers.
|
||||
//
|
||||
// This mechanism is intended only for trailers that are not known
|
||||
// prior to the headers being written. If the set of trailers is fixed
|
||||
// or known before the header is written, the normal Go trailers mechanism
|
||||
// is preferred:
|
||||
//
|
||||
// https://golang.org/pkg/net/http/#ResponseWriter
|
||||
// https://golang.org/pkg/net/http/#example_ResponseWriter_trailers
|
||||
const TrailerPrefix = "Trailer:"
|
||||
|
||||
// Push errors.
|
||||
var (
|
||||
ErrRecursivePush = errors.New("http2: recursive push not allowed")
|
||||
ErrPushLimitReached = errors.New("http2: push would exceed peer's SETTINGS_MAX_CONCURRENT_STREAMS")
|
||||
)
|
||||
|
||||
// ConfigureServer adds HTTP/2 support to a net/http Server.
|
||||
//
|
||||
// The configuration conf may be nil.
|
||||
//
|
||||
// ConfigureServer must be called before s begins serving.
|
||||
func ConfigureServer(s *http.Server, conf *Server) error {
|
||||
return configureServer(s, conf)
|
||||
}
|
||||
|
||||
// Server is an HTTP/2 server.
|
||||
type Server struct {
|
||||
// MaxHandlers limits the number of http.Handler ServeHTTP goroutines
|
||||
// which may run at a time over all connections.
|
||||
// Negative or zero no limit.
|
||||
// TODO: implement
|
||||
MaxHandlers int
|
||||
|
||||
// MaxConcurrentStreams optionally specifies the number of
|
||||
// concurrent streams that each client may have open at a
|
||||
// time. This is unrelated to the number of http.Handler goroutines
|
||||
// which may be active globally, which is MaxHandlers.
|
||||
// If zero, MaxConcurrentStreams defaults to at least 100, per
|
||||
// the HTTP/2 spec's recommendations.
|
||||
MaxConcurrentStreams uint32
|
||||
|
||||
// MaxDecoderHeaderTableSize optionally specifies the http2
|
||||
// SETTINGS_HEADER_TABLE_SIZE to send in the initial settings frame. It
|
||||
// informs the remote endpoint of the maximum size of the header compression
|
||||
// table used to decode header blocks, in octets. If zero, the default value
|
||||
// of 4096 is used.
|
||||
MaxDecoderHeaderTableSize uint32
|
||||
|
||||
// MaxEncoderHeaderTableSize optionally specifies an upper limit for the
|
||||
// header compression table used for encoding request headers. Received
|
||||
// SETTINGS_HEADER_TABLE_SIZE settings are capped at this limit. If zero,
|
||||
// the default value of 4096 is used.
|
||||
MaxEncoderHeaderTableSize uint32
|
||||
|
||||
// MaxReadFrameSize optionally specifies the largest frame
|
||||
// this server is willing to read. A valid value is between
|
||||
// 16k and 16M, inclusive. If zero or otherwise invalid, a
|
||||
// default value is used.
|
||||
MaxReadFrameSize uint32
|
||||
|
||||
// PermitProhibitedCipherSuites, if true, permits the use of
|
||||
// cipher suites prohibited by the HTTP/2 spec.
|
||||
PermitProhibitedCipherSuites bool
|
||||
|
||||
// IdleTimeout specifies how long until idle clients should be
|
||||
// closed with a GOAWAY frame. PING frames are not considered
|
||||
// activity for the purposes of IdleTimeout.
|
||||
// If zero or negative, there is no timeout.
|
||||
IdleTimeout time.Duration
|
||||
|
||||
// ReadIdleTimeout is the timeout after which a health check using a ping
|
||||
// frame will be carried out if no frame is received on the connection.
|
||||
// If zero, no health check is performed.
|
||||
ReadIdleTimeout time.Duration
|
||||
|
||||
// PingTimeout is the timeout after which the connection will be closed
|
||||
// if a response to a ping is not received.
|
||||
// If zero, a default of 15 seconds is used.
|
||||
PingTimeout time.Duration
|
||||
|
||||
// WriteByteTimeout is the timeout after which a connection will be
|
||||
// closed if no data can be written to it. The timeout begins when data is
|
||||
// available to write, and is extended whenever any bytes are written.
|
||||
// If zero or negative, there is no timeout.
|
||||
WriteByteTimeout time.Duration
|
||||
|
||||
// MaxUploadBufferPerConnection is the size of the initial flow
|
||||
// control window for each connections. The HTTP/2 spec does not
|
||||
// allow this to be smaller than 65535 or larger than 2^32-1.
|
||||
// If the value is outside this range, a default value will be
|
||||
// used instead.
|
||||
MaxUploadBufferPerConnection int32
|
||||
|
||||
// MaxUploadBufferPerStream is the size of the initial flow control
|
||||
// window for each stream. The HTTP/2 spec does not allow this to
|
||||
// be larger than 2^32-1. If the value is zero or larger than the
|
||||
// maximum, a default value will be used instead.
|
||||
MaxUploadBufferPerStream int32
|
||||
|
||||
// NewWriteScheduler constructs a write scheduler for a connection.
|
||||
// If nil, a default scheduler is chosen.
|
||||
//
|
||||
// Deprecated: User-provided write schedulers are deprecated.
|
||||
NewWriteScheduler func() WriteScheduler
|
||||
|
||||
// CountError, if non-nil, is called on HTTP/2 server errors.
|
||||
// It's intended to increment a metric for monitoring, such
|
||||
// as an expvar or Prometheus metric.
|
||||
// The errType consists of only ASCII word characters.
|
||||
CountError func(errType string)
|
||||
|
||||
// Internal state. This is a pointer (rather than embedded directly)
|
||||
// so that we don't embed a Mutex in this struct, which will make the
|
||||
// struct non-copyable, which might break some callers.
|
||||
state *serverInternalState
|
||||
}
|
||||
|
||||
// ServeConnOpts are options for the Server.ServeConn method.
|
||||
type ServeConnOpts struct {
|
||||
// Context is the base context to use.
|
||||
// If nil, context.Background is used.
|
||||
Context context.Context
|
||||
|
||||
// BaseConfig optionally sets the base configuration
|
||||
// for values. If nil, defaults are used.
|
||||
BaseConfig *http.Server
|
||||
|
||||
// Handler specifies which handler to use for processing
|
||||
// requests. If nil, BaseConfig.Handler is used. If BaseConfig
|
||||
// or BaseConfig.Handler is nil, http.DefaultServeMux is used.
|
||||
Handler http.Handler
|
||||
|
||||
// UpgradeRequest is an initial request received on a connection
|
||||
// undergoing an h2c upgrade. The request body must have been
|
||||
// completely read from the connection before calling ServeConn,
|
||||
// and the 101 Switching Protocols response written.
|
||||
UpgradeRequest *http.Request
|
||||
|
||||
// Settings is the decoded contents of the HTTP2-Settings header
|
||||
// in an h2c upgrade request.
|
||||
Settings []byte
|
||||
|
||||
// SawClientPreface is set if the HTTP/2 connection preface
|
||||
// has already been read from the connection.
|
||||
SawClientPreface bool
|
||||
}
|
||||
|
||||
// ServeConn serves HTTP/2 requests on the provided connection and
|
||||
// blocks until the connection is no longer readable.
|
||||
//
|
||||
// ServeConn starts speaking HTTP/2 assuming that c has not had any
|
||||
// reads or writes. It writes its initial settings frame and expects
|
||||
// to be able to read the preface and settings frame from the
|
||||
// client. If c has a ConnectionState method like a *tls.Conn, the
|
||||
// ConnectionState is used to verify the TLS ciphersuite and to set
|
||||
// the Request.TLS field in Handlers.
|
||||
//
|
||||
// ServeConn does not support h2c by itself. Any h2c support must be
|
||||
// implemented in terms of providing a suitably-behaving net.Conn.
|
||||
//
|
||||
// The opts parameter is optional. If nil, default values are used.
|
||||
func (s *Server) ServeConn(c net.Conn, opts *ServeConnOpts) {
|
||||
if opts == nil {
|
||||
opts = &ServeConnOpts{}
|
||||
}
|
||||
s.serveConn(c, opts, nil)
|
||||
}
|
||||
|
||||
func (o *ServeConnOpts) context() context.Context {
|
||||
if o != nil && o.Context != nil {
|
||||
return o.Context
|
||||
}
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
func (o *ServeConnOpts) baseConfig() *http.Server {
|
||||
if o != nil && o.BaseConfig != nil {
|
||||
return o.BaseConfig
|
||||
}
|
||||
return new(http.Server)
|
||||
}
|
||||
|
||||
func (o *ServeConnOpts) handler() http.Handler {
|
||||
if o != nil {
|
||||
if o.Handler != nil {
|
||||
return o.Handler
|
||||
}
|
||||
if o.BaseConfig != nil && o.BaseConfig.Handler != nil {
|
||||
return o.BaseConfig.Handler
|
||||
}
|
||||
}
|
||||
return http.DefaultServeMux
|
||||
}
|
||||
|
||||
func serverConnBaseContext(c net.Conn, opts *ServeConnOpts) (ctx context.Context, cancel func()) {
|
||||
ctx, cancel = context.WithCancel(opts.context())
|
||||
ctx = context.WithValue(ctx, http.LocalAddrContextKey, c.LocalAddr())
|
||||
if hs := opts.baseConfig(); hs != nil {
|
||||
ctx = context.WithValue(ctx, http.ServerContextKey, hs)
|
||||
}
|
||||
return
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
// Copyright 2026 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.27 && !http2legacy
|
||||
|
||||
// Server wrapping a net/http.Server.
|
||||
|
||||
package http2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type serverInternalState struct {
|
||||
s1 *http.Server
|
||||
initOnce sync.Once
|
||||
serveConnFunc func(context.Context, net.Conn, http.Handler, bool, *http.Request, []byte)
|
||||
}
|
||||
|
||||
func configureServer(s *http.Server, conf *Server) error {
|
||||
if s == nil {
|
||||
panic("nil *http.Server")
|
||||
}
|
||||
if conf == nil {
|
||||
conf = new(Server)
|
||||
}
|
||||
if conf.state != nil {
|
||||
// This isn't a panic in the pre-wrapping implementation,
|
||||
// but calling ConfigureServer twice with the same http2.Server
|
||||
// overwrites internal state on the server.
|
||||
// Make the error explicit and early here.
|
||||
panic("ConfigureServer may be called only once per Server")
|
||||
}
|
||||
if h1, h2 := s, conf; h2.IdleTimeout == 0 {
|
||||
if h1.IdleTimeout != 0 {
|
||||
h2.IdleTimeout = h1.IdleTimeout
|
||||
} else {
|
||||
h2.IdleTimeout = h1.ReadTimeout
|
||||
}
|
||||
}
|
||||
|
||||
// Register h2 and http/1.1 ALPN protocols on s.TLSConfig, matching
|
||||
// the pre-wrapping implementation in server.go, so that TLS listeners
|
||||
// built from s.TLSConfig still negotiate HTTP/2.
|
||||
if s.TLSConfig == nil {
|
||||
s.TLSConfig = new(tls.Config)
|
||||
}
|
||||
if !slices.Contains(s.TLSConfig.NextProtos, NextProtoTLS) {
|
||||
s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, NextProtoTLS)
|
||||
}
|
||||
if !slices.Contains(s.TLSConfig.NextProtos, "http/1.1") {
|
||||
s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, "http/1.1")
|
||||
}
|
||||
|
||||
conf.state = &serverInternalState{
|
||||
s1: s,
|
||||
}
|
||||
sconfig := &serverConfig{s: conf}
|
||||
if err := s.Serve(sconfig); err != nil || sconfig.serveConnFunc == nil {
|
||||
panic("http2: net/http does not support this version of x/net/http2")
|
||||
}
|
||||
conf.state.serveConnFunc = sconfig.serveConnFunc
|
||||
return nil
|
||||
}
|
||||
|
||||
type serverConfig struct {
|
||||
s *Server
|
||||
serveConnFunc func(context.Context, net.Conn, http.Handler, bool, *http.Request, []byte)
|
||||
}
|
||||
|
||||
func (*serverConfig) Accept() (net.Conn, error) {
|
||||
return nil, errors.New("unexpected call to Accept")
|
||||
}
|
||||
func (*serverConfig) Close() error {
|
||||
return nil
|
||||
}
|
||||
func (*serverConfig) Addr() net.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *serverConfig) ServeConnFunc(f func(context.Context, net.Conn, http.Handler, bool, *http.Request, []byte)) {
|
||||
s.serveConnFunc = f
|
||||
}
|
||||
|
||||
func (s *serverConfig) HTTP2Config() http.HTTP2Config {
|
||||
return http.HTTP2Config{
|
||||
MaxConcurrentStreams: int(s.s.MaxConcurrentStreams),
|
||||
MaxDecoderHeaderTableSize: int(s.s.MaxDecoderHeaderTableSize),
|
||||
MaxEncoderHeaderTableSize: int(s.s.MaxEncoderHeaderTableSize),
|
||||
MaxReadFrameSize: int(s.s.MaxReadFrameSize),
|
||||
PermitProhibitedCipherSuites: s.s.PermitProhibitedCipherSuites,
|
||||
MaxReceiveBufferPerConnection: int(s.s.MaxUploadBufferPerConnection),
|
||||
MaxReceiveBufferPerStream: int(s.s.MaxUploadBufferPerStream),
|
||||
SendPingTimeout: s.s.ReadIdleTimeout,
|
||||
PingTimeout: s.s.PingTimeout,
|
||||
WriteByteTimeout: s.s.WriteByteTimeout,
|
||||
CountError: s.s.CountError,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serverConfig) IdleTimeout() time.Duration {
|
||||
return s.s.IdleTimeout
|
||||
}
|
||||
|
||||
type serverConn struct{}
|
||||
|
||||
func (s *Server) serveConn(c net.Conn, opts *ServeConnOpts, _ func(*serverConn)) {
|
||||
var serveConnFunc func(context.Context, net.Conn, http.Handler, bool, *http.Request, []byte)
|
||||
switch {
|
||||
case opts.BaseConfig != nil:
|
||||
// The user has provided us with an http.Server to take configuration from.
|
||||
//
|
||||
// We can't send our request to opts.BaseConfig, because an http.Server can
|
||||
// only be associated with a single http2.Server and the user might
|
||||
// use this one with several http.Servers.
|
||||
//
|
||||
// We can't send our request to s.state.s1, because it doesn't contain
|
||||
// the right configuration.
|
||||
//
|
||||
// So create a one-off copy of opts.BaseConfig and use it.
|
||||
h1 := &http.Server{
|
||||
TLSConfig: opts.BaseConfig.TLSConfig,
|
||||
ReadTimeout: opts.BaseConfig.ReadTimeout,
|
||||
ReadHeaderTimeout: opts.BaseConfig.ReadHeaderTimeout,
|
||||
WriteTimeout: opts.BaseConfig.WriteTimeout,
|
||||
IdleTimeout: opts.BaseConfig.IdleTimeout,
|
||||
MaxHeaderBytes: opts.BaseConfig.MaxHeaderBytes,
|
||||
ConnState: opts.BaseConfig.ConnState,
|
||||
ErrorLog: opts.BaseConfig.ErrorLog,
|
||||
BaseContext: opts.BaseConfig.BaseContext,
|
||||
ConnContext: opts.BaseConfig.ConnContext,
|
||||
HTTP2: opts.BaseConfig.HTTP2,
|
||||
}
|
||||
sconfig := &serverConfig{s: s}
|
||||
if err := h1.Serve(sconfig); err != nil || sconfig.serveConnFunc == nil {
|
||||
panic("http2: net/http does not support this version of x/net/http2")
|
||||
}
|
||||
serveConnFunc = sconfig.serveConnFunc
|
||||
case s.state != nil:
|
||||
serveConnFunc = s.state.serveConnFunc
|
||||
default:
|
||||
// Strange-but-true: Server has no concurrency-safe way to initialize
|
||||
// its internal state, so historically ServeConn just doesn't use any
|
||||
// persistent state if you don't call ConfigureServer first.
|
||||
//
|
||||
// If ConfigureServer hasn't been called, create a one-off http.Server
|
||||
// for the connection, since we don't have any way to keep one around for reuse.
|
||||
h1 := &http.Server{}
|
||||
sconfig := &serverConfig{s: s}
|
||||
if err := h1.Serve(sconfig); err != nil || sconfig.serveConnFunc == nil {
|
||||
panic("http2: net/http does not support this version of x/net/http2")
|
||||
}
|
||||
serveConnFunc = sconfig.serveConnFunc
|
||||
}
|
||||
|
||||
ctx, cancel := serverConnBaseContext(c, opts)
|
||||
defer cancel()
|
||||
serveConnFunc(ctx, c, opts.handler(), opts.SawClientPreface, opts.UpgradeRequest, opts.Settings)
|
||||
|
||||
}
|
||||
|
||||
// FrameWriteRequest is a request to write a frame.
|
||||
//
|
||||
// Deprecated: User-provided write schedulers are deprecated.
|
||||
type FrameWriteRequest struct {
|
||||
// Ideally we'd define this in writesched_common.go,
|
||||
// to avoid duplicating an exported symbol across two files,
|
||||
// but the changes required to make this work are fairly large.
|
||||
}
|
||||
|
||||
func (wr FrameWriteRequest) StreamID() uint32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (wr FrameWriteRequest) DataSize() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (wr FrameWriteRequest) Consume(n int32) (FrameWriteRequest, FrameWriteRequest, int) {
|
||||
return FrameWriteRequest{}, FrameWriteRequest{}, 0
|
||||
}
|
||||
|
||||
func (wr FrameWriteRequest) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// NewPriorityWriteScheduler is deprecated.
|
||||
//
|
||||
// Deprecated: User-provided write schedulers are deprecated.
|
||||
func NewPriorityWriteScheduler(cfg *PriorityWriteSchedulerConfig) WriteScheduler {
|
||||
return unsupportedWriteScheduler{}
|
||||
}
|
||||
|
||||
// NewRandomWriteScheduler is deprecated.
|
||||
//
|
||||
// Deprecated: User-provided write schedulers are deprecated.
|
||||
func NewRandomWriteScheduler() WriteScheduler {
|
||||
return unsupportedWriteScheduler{}
|
||||
}
|
||||
|
||||
type unsupportedWriteScheduler struct{}
|
||||
|
||||
func (unsupportedWriteScheduler) OpenStream(streamID uint32, options OpenStreamOptions) {}
|
||||
func (unsupportedWriteScheduler) CloseStream(streamID uint32) {}
|
||||
func (unsupportedWriteScheduler) AdjustStream(streamID uint32, priority PriorityParam) {}
|
||||
func (unsupportedWriteScheduler) Push(wr FrameWriteRequest) {}
|
||||
func (unsupportedWriteScheduler) Pop() (wr FrameWriteRequest, ok bool) {
|
||||
return FrameWriteRequest{}, false
|
||||
}
|
||||
+28
-425
@@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !(go1.27 && !http2legacy)
|
||||
|
||||
// Transport code.
|
||||
|
||||
package http2
|
||||
@@ -21,20 +23,17 @@ import (
|
||||
"log"
|
||||
"math"
|
||||
"math/bits"
|
||||
mathrand "math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"golang.org/x/net/http2/hpack"
|
||||
"golang.org/x/net/idna"
|
||||
"golang.org/x/net/internal/httpcommon"
|
||||
)
|
||||
|
||||
@@ -60,123 +59,7 @@ const (
|
||||
defaultMaxConcurrentStreams = 1000
|
||||
)
|
||||
|
||||
// Transport is an HTTP/2 Transport.
|
||||
//
|
||||
// A Transport internally caches connections to servers. It is safe
|
||||
// for concurrent use by multiple goroutines.
|
||||
type Transport struct {
|
||||
// DialTLSContext specifies an optional dial function with context for
|
||||
// creating TLS connections for requests.
|
||||
//
|
||||
// If DialTLSContext and DialTLS is nil, tls.Dial is used.
|
||||
//
|
||||
// If the returned net.Conn has a ConnectionState method like tls.Conn,
|
||||
// it will be used to set http.Response.TLS.
|
||||
DialTLSContext func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error)
|
||||
|
||||
// DialTLS specifies an optional dial function for creating
|
||||
// TLS connections for requests.
|
||||
//
|
||||
// If DialTLSContext and DialTLS is nil, tls.Dial is used.
|
||||
//
|
||||
// Deprecated: Use DialTLSContext instead, which allows the transport
|
||||
// to cancel dials as soon as they are no longer needed.
|
||||
// If both are set, DialTLSContext takes priority.
|
||||
DialTLS func(network, addr string, cfg *tls.Config) (net.Conn, error)
|
||||
|
||||
// TLSClientConfig specifies the TLS configuration to use with
|
||||
// tls.Client. If nil, the default configuration is used.
|
||||
TLSClientConfig *tls.Config
|
||||
|
||||
// ConnPool optionally specifies an alternate connection pool to use.
|
||||
// If nil, the default is used.
|
||||
ConnPool ClientConnPool
|
||||
|
||||
// DisableCompression, if true, prevents the Transport from
|
||||
// requesting compression with an "Accept-Encoding: gzip"
|
||||
// request header when the Request contains no existing
|
||||
// Accept-Encoding value. If the Transport requests gzip on
|
||||
// its own and gets a gzipped response, it's transparently
|
||||
// decoded in the Response.Body. However, if the user
|
||||
// explicitly requested gzip it is not automatically
|
||||
// uncompressed.
|
||||
DisableCompression bool
|
||||
|
||||
// AllowHTTP, if true, permits HTTP/2 requests using the insecure,
|
||||
// plain-text "http" scheme. Note that this does not enable h2c support.
|
||||
AllowHTTP bool
|
||||
|
||||
// MaxHeaderListSize is the http2 SETTINGS_MAX_HEADER_LIST_SIZE to
|
||||
// send in the initial settings frame. It is how many bytes
|
||||
// of response headers are allowed. Unlike the http2 spec, zero here
|
||||
// means to use a default limit (currently 10MB). If you actually
|
||||
// want to advertise an unlimited value to the peer, Transport
|
||||
// interprets the highest possible value here (0xffffffff or 1<<32-1)
|
||||
// to mean no limit.
|
||||
MaxHeaderListSize uint32
|
||||
|
||||
// MaxReadFrameSize is the http2 SETTINGS_MAX_FRAME_SIZE to send in the
|
||||
// initial settings frame. It is the size in bytes of the largest frame
|
||||
// payload that the sender is willing to receive. If 0, no setting is
|
||||
// sent, and the value is provided by the peer, which should be 16384
|
||||
// according to the spec:
|
||||
// https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2.
|
||||
// Values are bounded in the range 16k to 16M.
|
||||
MaxReadFrameSize uint32
|
||||
|
||||
// MaxDecoderHeaderTableSize optionally specifies the http2
|
||||
// SETTINGS_HEADER_TABLE_SIZE to send in the initial settings frame. It
|
||||
// informs the remote endpoint of the maximum size of the header compression
|
||||
// table used to decode header blocks, in octets. If zero, the default value
|
||||
// of 4096 is used.
|
||||
MaxDecoderHeaderTableSize uint32
|
||||
|
||||
// MaxEncoderHeaderTableSize optionally specifies an upper limit for the
|
||||
// header compression table used for encoding request headers. Received
|
||||
// SETTINGS_HEADER_TABLE_SIZE settings are capped at this limit. If zero,
|
||||
// the default value of 4096 is used.
|
||||
MaxEncoderHeaderTableSize uint32
|
||||
|
||||
// StrictMaxConcurrentStreams controls whether the server's
|
||||
// SETTINGS_MAX_CONCURRENT_STREAMS should be respected
|
||||
// globally. If false, new TCP connections are created to the
|
||||
// server as needed to keep each under the per-connection
|
||||
// SETTINGS_MAX_CONCURRENT_STREAMS limit. If true, the
|
||||
// server's SETTINGS_MAX_CONCURRENT_STREAMS is interpreted as
|
||||
// a global limit and callers of RoundTrip block when needed,
|
||||
// waiting for their turn.
|
||||
StrictMaxConcurrentStreams bool
|
||||
|
||||
// IdleConnTimeout is the maximum amount of time an idle
|
||||
// (keep-alive) connection will remain idle before closing
|
||||
// itself.
|
||||
// Zero means no limit.
|
||||
IdleConnTimeout time.Duration
|
||||
|
||||
// ReadIdleTimeout is the timeout after which a health check using ping
|
||||
// frame will be carried out if no frame is received on the connection.
|
||||
// Note that a ping response will is considered a received frame, so if
|
||||
// there is no other traffic on the connection, the health check will
|
||||
// be performed every ReadIdleTimeout interval.
|
||||
// If zero, no health check is performed.
|
||||
ReadIdleTimeout time.Duration
|
||||
|
||||
// PingTimeout is the timeout after which the connection will be closed
|
||||
// if a response to Ping is not received.
|
||||
// Defaults to 15s.
|
||||
PingTimeout time.Duration
|
||||
|
||||
// WriteByteTimeout is the timeout after which the connection will be
|
||||
// closed no data can be written to it. The timeout begins when data is
|
||||
// available to write, and is extended whenever any bytes are written.
|
||||
WriteByteTimeout time.Duration
|
||||
|
||||
// CountError, if non-nil, is called on HTTP/2 transport errors.
|
||||
// It's intended to increment a metric for monitoring, such
|
||||
// as an expvar or Prometheus metric.
|
||||
// The errType consists of only ASCII word characters.
|
||||
CountError func(errType string)
|
||||
|
||||
type transportInternal struct {
|
||||
// t1, if non-nil, is the standard library Transport using
|
||||
// this transport. Its settings are used (but not its
|
||||
// RoundTrip method, etc).
|
||||
@@ -217,27 +100,18 @@ func (t *Transport) disableCompression() bool {
|
||||
return t.DisableCompression || (t.t1 != nil && t.t1.DisableCompression)
|
||||
}
|
||||
|
||||
// ConfigureTransport configures a net/http HTTP/1 Transport to use HTTP/2.
|
||||
// It returns an error if t1 has already been HTTP/2-enabled.
|
||||
//
|
||||
// Use ConfigureTransports instead to configure the HTTP/2 Transport.
|
||||
func ConfigureTransport(t1 *http.Transport) error {
|
||||
_, err := ConfigureTransports(t1)
|
||||
func configureTransport(t1 *http.Transport) error {
|
||||
_, err := configureTransports(t1)
|
||||
return err
|
||||
}
|
||||
|
||||
// ConfigureTransports configures a net/http HTTP/1 Transport to use HTTP/2.
|
||||
// It returns a new HTTP/2 Transport for further configuration.
|
||||
// It returns an error if t1 has already been HTTP/2-enabled.
|
||||
func ConfigureTransports(t1 *http.Transport) (*Transport, error) {
|
||||
return configureTransports(t1)
|
||||
}
|
||||
|
||||
func configureTransports(t1 *http.Transport) (*Transport, error) {
|
||||
connPool := new(clientConnPool)
|
||||
t2 := &Transport{
|
||||
ConnPool: noDialClientConnPool{connPool},
|
||||
t1: t1,
|
||||
transportInternal: transportInternal{
|
||||
t1: t1,
|
||||
},
|
||||
}
|
||||
connPool.t = t2
|
||||
if err := registerHTTPSProtocol(t1, noDialH2RoundTripper{t2}); err != nil {
|
||||
@@ -525,68 +399,7 @@ func (sew stickyErrWriter) Write(p []byte) (n int, err error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// noCachedConnError is the concrete type of ErrNoCachedConn, which
|
||||
// needs to be detected by net/http regardless of whether it's its
|
||||
// bundled version (in h2_bundle.go with a rewritten type name) or
|
||||
// from a user's x/net/http2. As such, as it has a unique method name
|
||||
// (IsHTTP2NoCachedConnError) that net/http sniffs for via func
|
||||
// isNoCachedConnError.
|
||||
type noCachedConnError struct{}
|
||||
|
||||
func (noCachedConnError) IsHTTP2NoCachedConnError() {}
|
||||
func (noCachedConnError) Error() string { return "http2: no cached connection was available" }
|
||||
|
||||
// isNoCachedConnError reports whether err is of type noCachedConnError
|
||||
// or its equivalent renamed type in net/http2's h2_bundle.go. Both types
|
||||
// may coexist in the same running program.
|
||||
func isNoCachedConnError(err error) bool {
|
||||
_, ok := err.(interface{ IsHTTP2NoCachedConnError() })
|
||||
return ok
|
||||
}
|
||||
|
||||
var ErrNoCachedConn error = noCachedConnError{}
|
||||
|
||||
// RoundTripOpt are options for the Transport.RoundTripOpt method.
|
||||
type RoundTripOpt struct {
|
||||
// OnlyCachedConn controls whether RoundTripOpt may
|
||||
// create a new TCP connection. If set true and
|
||||
// no cached connection is available, RoundTripOpt
|
||||
// will return ErrNoCachedConn.
|
||||
OnlyCachedConn bool
|
||||
|
||||
allowHTTP bool // allow http:// URLs
|
||||
}
|
||||
|
||||
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return t.RoundTripOpt(req, RoundTripOpt{})
|
||||
}
|
||||
|
||||
// authorityAddr returns a given authority (a host/IP, or host:port / ip:port)
|
||||
// and returns a host:port. The port 443 is added if needed.
|
||||
func authorityAddr(scheme string, authority string) (addr string) {
|
||||
host, port, err := net.SplitHostPort(authority)
|
||||
if err != nil { // authority didn't have a port
|
||||
host = authority
|
||||
port = ""
|
||||
}
|
||||
if port == "" { // authority's port was empty
|
||||
port = "443"
|
||||
if scheme == "http" {
|
||||
port = "80"
|
||||
}
|
||||
}
|
||||
if a, err := idna.ToASCII(host); err == nil {
|
||||
host = a
|
||||
}
|
||||
// IPv6 address literal, without a port:
|
||||
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
||||
return host + ":" + port
|
||||
}
|
||||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
switch req.URL.Scheme {
|
||||
case "https":
|
||||
// Always okay.
|
||||
@@ -597,126 +410,15 @@ func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res
|
||||
default:
|
||||
return nil, errors.New("http2: unsupported scheme")
|
||||
}
|
||||
|
||||
addr := authorityAddr(req.URL.Scheme, req.URL.Host)
|
||||
for retry := 0; ; retry++ {
|
||||
cc, err := t.connPool().GetClientConn(req, addr)
|
||||
if err != nil {
|
||||
t.vlogf("http2: Transport failed to get client conn for %s: %v", addr, err)
|
||||
return nil, err
|
||||
}
|
||||
reused := !atomic.CompareAndSwapUint32(&cc.atomicReused, 0, 1)
|
||||
traceGotConn(req, cc, reused)
|
||||
res, err := cc.RoundTrip(req)
|
||||
if err != nil && retry <= 6 {
|
||||
roundTripErr := err
|
||||
if req, err = shouldRetryRequest(req, err); err == nil {
|
||||
// After the first retry, do exponential backoff with 10% jitter.
|
||||
if retry == 0 {
|
||||
t.vlogf("RoundTrip retrying after failure: %v", roundTripErr)
|
||||
continue
|
||||
}
|
||||
backoff := float64(uint(1) << (uint(retry) - 1))
|
||||
backoff += backoff * (0.1 * mathrand.Float64())
|
||||
d := time.Second * time.Duration(backoff)
|
||||
tm := time.NewTimer(d)
|
||||
select {
|
||||
case <-tm.C:
|
||||
t.vlogf("RoundTrip retrying after failure: %v", roundTripErr)
|
||||
continue
|
||||
case <-req.Context().Done():
|
||||
tm.Stop()
|
||||
err = req.Context().Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
t.vlogf("RoundTrip failure: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
return t.roundTripViaPool(req, opt, t.connPool())
|
||||
}
|
||||
|
||||
// CloseIdleConnections closes any connections which were previously
|
||||
// connected from previous requests but are now sitting idle.
|
||||
// It does not interrupt any connections currently in use.
|
||||
func (t *Transport) CloseIdleConnections() {
|
||||
func (t *Transport) closeIdleConnections() {
|
||||
if cp, ok := t.connPool().(clientConnPoolIdleCloser); ok {
|
||||
cp.closeIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
errClientConnClosed = errors.New("http2: client conn is closed")
|
||||
errClientConnUnusable = errors.New("http2: client conn not usable")
|
||||
errClientConnNotEstablished = errors.New("http2: client conn could not be established")
|
||||
errClientConnGotGoAway = errors.New("http2: Transport received Server's graceful shutdown GOAWAY")
|
||||
errClientConnForceClosed = errors.New("http2: client connection force closed via ClientConn.Close")
|
||||
)
|
||||
|
||||
// shouldRetryRequest is called by RoundTrip when a request fails to get
|
||||
// response headers. It is always called with a non-nil error.
|
||||
// It returns either a request to retry (either the same request, or a
|
||||
// modified clone), or an error if the request can't be replayed.
|
||||
func shouldRetryRequest(req *http.Request, err error) (*http.Request, error) {
|
||||
if !canRetryError(err) {
|
||||
return nil, err
|
||||
}
|
||||
// If the Body is nil (or http.NoBody), it's safe to reuse
|
||||
// this request and its Body.
|
||||
if req.Body == nil || req.Body == http.NoBody {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// If the request body can be reset back to its original
|
||||
// state via the optional req.GetBody, do that.
|
||||
if req.GetBody != nil {
|
||||
body, err := req.GetBody()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newReq := *req
|
||||
newReq.Body = body
|
||||
return &newReq, nil
|
||||
}
|
||||
|
||||
// The Request.Body can't reset back to the beginning, but we
|
||||
// don't seem to have started to read from it yet, so reuse
|
||||
// the request directly.
|
||||
if err == errClientConnUnusable {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("http2: Transport: cannot retry err [%v] after Request.Body was written; define Request.GetBody to avoid this error", err)
|
||||
}
|
||||
|
||||
func canRetryError(err error) bool {
|
||||
if err == errClientConnUnusable || err == errClientConnGotGoAway {
|
||||
return true
|
||||
}
|
||||
if se, ok := err.(StreamError); ok {
|
||||
return se.Code == ErrCodeRefusedStream
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Transport) dialClientConn(ctx context.Context, addr string, singleUse bool) (*ClientConn, error) {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
@@ -743,27 +445,6 @@ func (t *Transport) newTLSConfig(host string) *tls.Config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (t *Transport) dialTLS(ctx context.Context, network, addr string, tlsCfg *tls.Config) (net.Conn, error) {
|
||||
if t.DialTLSContext != nil {
|
||||
return t.DialTLSContext(ctx, network, addr, tlsCfg)
|
||||
} else if t.DialTLS != nil {
|
||||
return t.DialTLS(network, addr, tlsCfg)
|
||||
}
|
||||
|
||||
tlsCn, err := t.dialTLSWithContext(ctx, network, addr, tlsCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state := tlsCn.ConnectionState()
|
||||
if p := state.NegotiatedProtocol; p != NextProtoTLS {
|
||||
return nil, fmt.Errorf("http2: unexpected ALPN protocol %q; want %q", p, NextProtoTLS)
|
||||
}
|
||||
if !state.NegotiatedProtocolIsMutual {
|
||||
return nil, errors.New("http2: could not negotiate protocol mutually")
|
||||
}
|
||||
return tlsCn, nil
|
||||
}
|
||||
|
||||
// disableKeepAlives reports whether connections should be closed as
|
||||
// soon as possible after handling the first request.
|
||||
func (t *Transport) disableKeepAlives() bool {
|
||||
@@ -777,7 +458,7 @@ func (t *Transport) expectContinueTimeout() time.Duration {
|
||||
return t.t1.ExpectContinueTimeout
|
||||
}
|
||||
|
||||
func (t *Transport) NewClientConn(c net.Conn) (*ClientConn, error) {
|
||||
func (t *Transport) newUserClientConn(c net.Conn) (*ClientConn, error) {
|
||||
return t.newClientConn(c, t.disableKeepAlives(), nil)
|
||||
}
|
||||
|
||||
@@ -890,8 +571,7 @@ func (cc *ClientConn) healthCheck() {
|
||||
}
|
||||
}
|
||||
|
||||
// SetDoNotReuse marks cc as not reusable for future HTTP requests.
|
||||
func (cc *ClientConn) SetDoNotReuse() {
|
||||
func (cc *ClientConn) setDoNotReuse() {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
cc.doNotReuse = true
|
||||
@@ -932,21 +612,13 @@ func (cc *ClientConn) setGoAway(f *GoAwayFrame) {
|
||||
}
|
||||
}
|
||||
|
||||
// CanTakeNewRequest reports whether the connection can take a new request,
|
||||
// meaning it has not been closed or received or sent a GOAWAY.
|
||||
//
|
||||
// If the caller is going to immediately make a new request on this
|
||||
// connection, use ReserveNewRequest instead.
|
||||
func (cc *ClientConn) CanTakeNewRequest() bool {
|
||||
func (cc *ClientConn) canTakeNewRequest() bool {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
return cc.canTakeNewRequestLocked()
|
||||
}
|
||||
|
||||
// ReserveNewRequest is like CanTakeNewRequest but also reserves a
|
||||
// concurrent stream in cc. The reservation is decremented on the
|
||||
// next call to RoundTrip.
|
||||
func (cc *ClientConn) ReserveNewRequest() bool {
|
||||
func (cc *ClientConn) reserveNewRequest() bool {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
if st := cc.idleStateLocked(); !st.canTakeNewRequest {
|
||||
@@ -956,41 +628,7 @@ func (cc *ClientConn) ReserveNewRequest() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ClientConnState describes the state of a ClientConn.
|
||||
type ClientConnState struct {
|
||||
// Closed is whether the connection is closed.
|
||||
Closed bool
|
||||
|
||||
// Closing is whether the connection is in the process of
|
||||
// closing. It may be closing due to shutdown, being a
|
||||
// single-use connection, being marked as DoNotReuse, or
|
||||
// having received a GOAWAY frame.
|
||||
Closing bool
|
||||
|
||||
// StreamsActive is how many streams are active.
|
||||
StreamsActive int
|
||||
|
||||
// StreamsReserved is how many streams have been reserved via
|
||||
// ClientConn.ReserveNewRequest.
|
||||
StreamsReserved int
|
||||
|
||||
// StreamsPending is how many requests have been sent in excess
|
||||
// of the peer's advertised MaxConcurrentStreams setting and
|
||||
// are waiting for other streams to complete.
|
||||
StreamsPending int
|
||||
|
||||
// MaxConcurrentStreams is how many concurrent streams the
|
||||
// peer advertised as acceptable. Zero means no SETTINGS
|
||||
// frame has been received yet.
|
||||
MaxConcurrentStreams uint32
|
||||
|
||||
// LastIdle, if non-zero, is when the connection last
|
||||
// transitioned to idle state.
|
||||
LastIdle time.Time
|
||||
}
|
||||
|
||||
// State returns a snapshot of cc's state.
|
||||
func (cc *ClientConn) State() ClientConnState {
|
||||
func (cc *ClientConn) state() ClientConnState {
|
||||
cc.wmu.Lock()
|
||||
maxConcurrent := cc.maxConcurrentStreams
|
||||
if !cc.seenSettings {
|
||||
@@ -1161,6 +799,12 @@ func (cc *ClientConn) closeIfIdle() {
|
||||
cc.closeConn()
|
||||
}
|
||||
|
||||
func (cc *ClientConn) stopIdleTimer() {
|
||||
if cc.idleTimer != nil {
|
||||
cc.idleTimer.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *ClientConn) isDoNotReuseAndIdle() bool {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
@@ -1169,8 +813,7 @@ func (cc *ClientConn) isDoNotReuseAndIdle() bool {
|
||||
|
||||
var shutdownEnterWaitStateHook = func() {}
|
||||
|
||||
// Shutdown gracefully closes the client connection, waiting for running streams to complete.
|
||||
func (cc *ClientConn) Shutdown(ctx context.Context) error {
|
||||
func (cc *ClientConn) shutdown(ctx context.Context) error {
|
||||
if err := cc.sendGoAway(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1244,10 +887,7 @@ func (cc *ClientConn) closeForError(err error) {
|
||||
cc.closeConn()
|
||||
}
|
||||
|
||||
// Close closes the client connection immediately.
|
||||
//
|
||||
// In-flight requests are interrupted. For a graceful shutdown, use Shutdown instead.
|
||||
func (cc *ClientConn) Close() error {
|
||||
func (cc *ClientConn) close() error {
|
||||
cc.closeForError(errClientConnForceClosed)
|
||||
return nil
|
||||
}
|
||||
@@ -1301,11 +941,11 @@ func (cc *ClientConn) decrStreamReservationsLocked() {
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return cc.roundTrip(req, nil)
|
||||
func (cc *ClientConn) roundTrip(req *http.Request) (*http.Response, error) {
|
||||
return cc.internalRoundTrip(req, nil)
|
||||
}
|
||||
|
||||
func (cc *ClientConn) roundTrip(req *http.Request, streamf func(*clientStream)) (*http.Response, error) {
|
||||
func (cc *ClientConn) internalRoundTrip(req *http.Request, streamf func(*clientStream)) (*http.Response, error) {
|
||||
ctx := req.Context()
|
||||
cs := &clientStream{
|
||||
cc: cc,
|
||||
@@ -2125,19 +1765,6 @@ func (cc *ClientConn) readLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
// GoAwayError is returned by the Transport when the server closes the
|
||||
// TCP connection after sending a GOAWAY frame.
|
||||
type GoAwayError struct {
|
||||
LastStreamID uint32
|
||||
ErrCode ErrCode
|
||||
DebugData string
|
||||
}
|
||||
|
||||
func (e GoAwayError) Error() string {
|
||||
return fmt.Sprintf("http2: server sent GOAWAY and closed the connection; LastStreamID=%v, ErrCode=%v, debug=%q",
|
||||
e.LastStreamID, e.ErrCode, e.DebugData)
|
||||
}
|
||||
|
||||
func isEOFOrNetReadError(err error) bool {
|
||||
if err == io.EOF {
|
||||
return true
|
||||
@@ -2978,7 +2605,7 @@ func (rl *clientConnReadLoop) processResetStream(f *RSTStreamFrame) error {
|
||||
}
|
||||
|
||||
// Ping sends a PING frame to the server and waits for the ack.
|
||||
func (cc *ClientConn) Ping(ctx context.Context) error {
|
||||
func (cc *ClientConn) ping(ctx context.Context) error {
|
||||
c := make(chan struct{})
|
||||
// Generate a random payload
|
||||
var p [8]byte
|
||||
@@ -3092,16 +2719,6 @@ func (cc *ClientConn) vlogf(format string, args ...interface{}) {
|
||||
cc.t.vlogf(format, args...)
|
||||
}
|
||||
|
||||
func (t *Transport) vlogf(format string, args ...interface{}) {
|
||||
if VerboseLogs {
|
||||
t.logf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) logf(format string, args ...interface{}) {
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
|
||||
var noBody io.ReadCloser = noBodyReader{}
|
||||
|
||||
type noBodyReader struct{}
|
||||
@@ -3417,17 +3034,3 @@ func traceGot1xxResponseFunc(trace *httptrace.ClientTrace) func(int, textproto.M
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dialTLSWithContext uses tls.Dialer, added in Go 1.15, to open a TLS
|
||||
// connection.
|
||||
func (t *Transport) dialTLSWithContext(ctx context.Context, network, addr string, cfg *tls.Config) (*tls.Conn, error) {
|
||||
dialer := &tls.Dialer{
|
||||
Config: cfg,
|
||||
}
|
||||
cn, err := dialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsCn := cn.(*tls.Conn) // DialContext comment promises this will always succeed
|
||||
return tlsCn, nil
|
||||
}
|
||||
|
||||
+447
@@ -0,0 +1,447 @@
|
||||
// Copyright 2026 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package http2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
mathrand "math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
// ConfigureTransport configures a net/http HTTP/1 Transport to use HTTP/2.
|
||||
// It returns an error if t1 has already been HTTP/2-enabled.
|
||||
//
|
||||
// Use ConfigureTransports instead to configure the HTTP/2 Transport.
|
||||
func ConfigureTransport(t1 *http.Transport) error {
|
||||
return configureTransport(t1)
|
||||
}
|
||||
|
||||
// ConfigureTransports configures a net/http HTTP/1 Transport to use HTTP/2.
|
||||
// It returns a new HTTP/2 Transport for further configuration.
|
||||
// It returns an error if t1 has already been HTTP/2-enabled.
|
||||
func ConfigureTransports(t1 *http.Transport) (*Transport, error) {
|
||||
return configureTransports(t1)
|
||||
}
|
||||
|
||||
// Transport is an HTTP/2 Transport.
|
||||
//
|
||||
// A Transport internally caches connections to servers. It is safe
|
||||
// for concurrent use by multiple goroutines.
|
||||
type Transport struct {
|
||||
// DialTLSContext specifies an optional dial function with context for
|
||||
// creating TLS connections for requests.
|
||||
//
|
||||
// If DialTLSContext and DialTLS is nil, tls.Dial is used.
|
||||
//
|
||||
// If the returned net.Conn has a ConnectionState method like tls.Conn,
|
||||
// it will be used to set http.Response.TLS.
|
||||
DialTLSContext func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error)
|
||||
|
||||
// DialTLS specifies an optional dial function for creating
|
||||
// TLS connections for requests.
|
||||
//
|
||||
// If DialTLSContext and DialTLS is nil, tls.Dial is used.
|
||||
//
|
||||
// Deprecated: Use DialTLSContext instead, which allows the transport
|
||||
// to cancel dials as soon as they are no longer needed.
|
||||
// If both are set, DialTLSContext takes priority.
|
||||
DialTLS func(network, addr string, cfg *tls.Config) (net.Conn, error)
|
||||
|
||||
// TLSClientConfig specifies the TLS configuration to use with
|
||||
// tls.Client. If nil, the default configuration is used.
|
||||
TLSClientConfig *tls.Config
|
||||
|
||||
// ConnPool optionally specifies an alternate connection pool to use.
|
||||
// If nil, the default is used.
|
||||
ConnPool ClientConnPool
|
||||
|
||||
// DisableCompression, if true, prevents the Transport from
|
||||
// requesting compression with an "Accept-Encoding: gzip"
|
||||
// request header when the Request contains no existing
|
||||
// Accept-Encoding value. If the Transport requests gzip on
|
||||
// its own and gets a gzipped response, it's transparently
|
||||
// decoded in the Response.Body. However, if the user
|
||||
// explicitly requested gzip it is not automatically
|
||||
// uncompressed.
|
||||
DisableCompression bool
|
||||
|
||||
// AllowHTTP, if true, permits HTTP/2 requests using the insecure,
|
||||
// plain-text "http" scheme. Note that this does not enable h2c support.
|
||||
AllowHTTP bool
|
||||
|
||||
// MaxHeaderListSize is the http2 SETTINGS_MAX_HEADER_LIST_SIZE to
|
||||
// send in the initial settings frame. It is how many bytes
|
||||
// of response headers are allowed. Unlike the http2 spec, zero here
|
||||
// means to use a default limit (currently 10MB). If you actually
|
||||
// want to advertise an unlimited value to the peer, Transport
|
||||
// interprets the highest possible value here (0xffffffff or 1<<32-1)
|
||||
// to mean no limit.
|
||||
MaxHeaderListSize uint32
|
||||
|
||||
// MaxReadFrameSize is the http2 SETTINGS_MAX_FRAME_SIZE to send in the
|
||||
// initial settings frame. It is the size in bytes of the largest frame
|
||||
// payload that the sender is willing to receive. If 0, no setting is
|
||||
// sent, and the value is provided by the peer, which should be 16384
|
||||
// according to the spec:
|
||||
// https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2.
|
||||
// Values are bounded in the range 16k to 16M.
|
||||
MaxReadFrameSize uint32
|
||||
|
||||
// MaxDecoderHeaderTableSize optionally specifies the http2
|
||||
// SETTINGS_HEADER_TABLE_SIZE to send in the initial settings frame. It
|
||||
// informs the remote endpoint of the maximum size of the header compression
|
||||
// table used to decode header blocks, in octets. If zero, the default value
|
||||
// of 4096 is used.
|
||||
MaxDecoderHeaderTableSize uint32
|
||||
|
||||
// MaxEncoderHeaderTableSize optionally specifies an upper limit for the
|
||||
// header compression table used for encoding request headers. Received
|
||||
// SETTINGS_HEADER_TABLE_SIZE settings are capped at this limit. If zero,
|
||||
// the default value of 4096 is used.
|
||||
MaxEncoderHeaderTableSize uint32
|
||||
|
||||
// StrictMaxConcurrentStreams controls whether the server's
|
||||
// SETTINGS_MAX_CONCURRENT_STREAMS should be respected
|
||||
// globally. If false, new TCP connections are created to the
|
||||
// server as needed to keep each under the per-connection
|
||||
// SETTINGS_MAX_CONCURRENT_STREAMS limit. If true, the
|
||||
// server's SETTINGS_MAX_CONCURRENT_STREAMS is interpreted as
|
||||
// a global limit and callers of RoundTrip block when needed,
|
||||
// waiting for their turn.
|
||||
StrictMaxConcurrentStreams bool
|
||||
|
||||
// IdleConnTimeout is the maximum amount of time an idle
|
||||
// (keep-alive) connection will remain idle before closing
|
||||
// itself.
|
||||
// Zero means no limit.
|
||||
IdleConnTimeout time.Duration
|
||||
|
||||
// ReadIdleTimeout is the timeout after which a health check using ping
|
||||
// frame will be carried out if no frame is received on the connection.
|
||||
// Note that a ping response will is considered a received frame, so if
|
||||
// there is no other traffic on the connection, the health check will
|
||||
// be performed every ReadIdleTimeout interval.
|
||||
// If zero, no health check is performed.
|
||||
ReadIdleTimeout time.Duration
|
||||
|
||||
// PingTimeout is the timeout after which the connection will be closed
|
||||
// if a response to Ping is not received.
|
||||
// Defaults to 15s.
|
||||
PingTimeout time.Duration
|
||||
|
||||
// WriteByteTimeout is the timeout after which the connection will be
|
||||
// closed no data can be written to it. The timeout begins when data is
|
||||
// available to write, and is extended whenever any bytes are written.
|
||||
WriteByteTimeout time.Duration
|
||||
|
||||
// CountError, if non-nil, is called on HTTP/2 transport errors.
|
||||
// It's intended to increment a metric for monitoring, such
|
||||
// as an expvar or Prometheus metric.
|
||||
// The errType consists of only ASCII word characters.
|
||||
CountError func(errType string)
|
||||
|
||||
// Internal state, differs between wrapped and non-wrapped implementations.
|
||||
transportInternal
|
||||
}
|
||||
|
||||
var (
|
||||
errClientConnClosed = errors.New("http2: client conn is closed")
|
||||
errClientConnNotEstablished = errors.New("http2: client conn could not be established")
|
||||
errClientConnGotGoAway = errors.New("http2: Transport received Server's graceful shutdown GOAWAY")
|
||||
errClientConnForceClosed = errors.New("http2: client connection force closed via ClientConn.Close")
|
||||
errClientConnUnusable = errors.New("http2: client conn not usable")
|
||||
)
|
||||
|
||||
// ClientConnPool manages a pool of HTTP/2 client connections.
|
||||
type ClientConnPool interface {
|
||||
// GetClientConn returns a specific HTTP/2 connection (usually
|
||||
// a TLS-TCP connection) to an HTTP/2 server. On success, the
|
||||
// returned ClientConn accounts for the upcoming RoundTrip
|
||||
// call, so the caller should not omit it. If the caller needs
|
||||
// to, ClientConn.RoundTrip can be called with a bogus
|
||||
// new(http.Request) to release the stream reservation.
|
||||
GetClientConn(req *http.Request, addr string) (*ClientConn, error)
|
||||
MarkDead(*ClientConn)
|
||||
}
|
||||
|
||||
// ClientConnState describes the state of a ClientConn.
|
||||
type ClientConnState struct {
|
||||
// Closed is whether the connection is closed.
|
||||
Closed bool
|
||||
|
||||
// Closing is whether the connection is in the process of
|
||||
// closing. It may be closing due to shutdown, being a
|
||||
// single-use connection, being marked as DoNotReuse, or
|
||||
// having received a GOAWAY frame.
|
||||
Closing bool
|
||||
|
||||
// StreamsActive is how many streams are active.
|
||||
StreamsActive int
|
||||
|
||||
// StreamsReserved is how many streams have been reserved via
|
||||
// ClientConn.ReserveNewRequest.
|
||||
StreamsReserved int
|
||||
|
||||
// StreamsPending is how many requests have been sent in excess
|
||||
// of the peer's advertised MaxConcurrentStreams setting and
|
||||
// are waiting for other streams to complete.
|
||||
StreamsPending int
|
||||
|
||||
// MaxConcurrentStreams is how many concurrent streams the
|
||||
// peer advertised as acceptable. Zero means no SETTINGS
|
||||
// frame has been received yet.
|
||||
MaxConcurrentStreams uint32
|
||||
|
||||
// LastIdle, if non-zero, is when the connection last
|
||||
// transitioned to idle state.
|
||||
LastIdle time.Time
|
||||
}
|
||||
|
||||
// RoundTripOpt are options for the Transport.RoundTripOpt method.
|
||||
type RoundTripOpt struct {
|
||||
// OnlyCachedConn controls whether RoundTripOpt may
|
||||
// create a new TCP connection. If set true and
|
||||
// no cached connection is available, RoundTripOpt
|
||||
// will return ErrNoCachedConn.
|
||||
|
||||
// OnlyCachedConn was broken in https://go.dev/cl/16699.
|
||||
OnlyCachedConn bool
|
||||
|
||||
allowHTTP bool // allow http:// URLs
|
||||
}
|
||||
|
||||
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return t.RoundTripOpt(req, RoundTripOpt{})
|
||||
}
|
||||
|
||||
// RoundTripOpt is like RoundTrip, but takes options.
|
||||
func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
|
||||
return t.roundTripOpt(req, opt)
|
||||
}
|
||||
|
||||
// CloseIdleConnections closes any connections which were previously
|
||||
// connected from previous requests but are now sitting idle.
|
||||
// It does not interrupt any connections currently in use.
|
||||
func (t *Transport) CloseIdleConnections() {
|
||||
t.closeIdleConnections()
|
||||
}
|
||||
|
||||
func (t *Transport) NewClientConn(c net.Conn) (*ClientConn, error) {
|
||||
return t.newUserClientConn(c)
|
||||
}
|
||||
|
||||
// authorityAddr returns a given authority (a host/IP, or host:port / ip:port)
|
||||
// and returns a host:port. The port 443 is added if needed.
|
||||
func authorityAddr(scheme string, authority string) (addr string) {
|
||||
host, port, err := net.SplitHostPort(authority)
|
||||
if err != nil { // authority didn't have a port
|
||||
host = authority
|
||||
port = ""
|
||||
}
|
||||
if port == "" { // authority's port was empty
|
||||
port = "443"
|
||||
if scheme == "http" {
|
||||
port = "80"
|
||||
}
|
||||
}
|
||||
if a, err := idna.ToASCII(host); err == nil {
|
||||
host = a
|
||||
}
|
||||
// IPv6 address literal, without a port:
|
||||
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
||||
return host + ":" + port
|
||||
}
|
||||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
func (t *Transport) roundTripViaPool(req *http.Request, opt RoundTripOpt, pool ClientConnPool) (*http.Response, error) {
|
||||
addr := authorityAddr(req.URL.Scheme, req.URL.Host)
|
||||
for retry := 0; ; retry++ {
|
||||
cc, err := pool.GetClientConn(req, addr)
|
||||
if err != nil {
|
||||
t.vlogf("http2: Transport failed to get client conn for %s: %v", addr, err)
|
||||
return nil, err
|
||||
}
|
||||
reused := !atomic.CompareAndSwapUint32(&cc.atomicReused, 0, 1)
|
||||
traceGotConn(req, cc, reused)
|
||||
res, err := cc.RoundTrip(req)
|
||||
if err != nil && retry <= 6 {
|
||||
roundTripErr := err
|
||||
if req, err = shouldRetryRequest(req, err); err == nil {
|
||||
// After the first retry, do exponential backoff with 10% jitter.
|
||||
if retry == 0 {
|
||||
t.vlogf("RoundTrip retrying after failure: %v", roundTripErr)
|
||||
continue
|
||||
}
|
||||
backoff := float64(uint(1) << (uint(retry) - 1))
|
||||
backoff += backoff * (0.1 * mathrand.Float64())
|
||||
d := time.Second * time.Duration(backoff)
|
||||
tm := time.NewTimer(d)
|
||||
select {
|
||||
case <-tm.C:
|
||||
t.vlogf("RoundTrip retrying after failure: %v", roundTripErr)
|
||||
continue
|
||||
case <-req.Context().Done():
|
||||
tm.Stop()
|
||||
err = req.Context().Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
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.
|
||||
cc.stopIdleTimer()
|
||||
pool.MarkDead(cc)
|
||||
}
|
||||
if err != nil {
|
||||
t.vlogf("RoundTrip failure: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
// shouldRetryRequest is called by RoundTrip when a request fails to get
|
||||
// response headers. It is always called with a non-nil error.
|
||||
// It returns either a request to retry (either the same request, or a
|
||||
// modified clone), or an error if the request can't be replayed.
|
||||
func shouldRetryRequest(req *http.Request, err error) (*http.Request, error) {
|
||||
if !canRetryError(err) {
|
||||
return nil, err
|
||||
}
|
||||
// If the Body is nil (or http.NoBody), it's safe to reuse
|
||||
// this request and its Body.
|
||||
if req.Body == nil || req.Body == http.NoBody {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// If the request body can be reset back to its original
|
||||
// state via the optional req.GetBody, do that.
|
||||
if req.GetBody != nil {
|
||||
body, err := req.GetBody()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newReq := *req
|
||||
newReq.Body = body
|
||||
return &newReq, nil
|
||||
}
|
||||
|
||||
// The Request.Body can't reset back to the beginning, but we
|
||||
// don't seem to have started to read from it yet, so reuse
|
||||
// the request directly.
|
||||
if err == errClientConnUnusable {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("http2: Transport: cannot retry err [%v] after Request.Body was written; define Request.GetBody to avoid this error", err)
|
||||
}
|
||||
|
||||
func canRetryError(err error) bool {
|
||||
if err == errClientConnUnusable || err == errClientConnGotGoAway {
|
||||
return true
|
||||
}
|
||||
if se, ok := err.(StreamError); ok {
|
||||
return se.Code == ErrCodeRefusedStream
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Transport) vlogf(format string, args ...interface{}) {
|
||||
if VerboseLogs {
|
||||
t.logf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) logf(format string, args ...interface{}) {
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
|
||||
func (t *Transport) dialTLS(ctx context.Context, network, addr string, tlsCfg *tls.Config) (net.Conn, error) {
|
||||
if t.DialTLSContext != nil {
|
||||
return t.DialTLSContext(ctx, network, addr, tlsCfg)
|
||||
} else if t.DialTLS != nil {
|
||||
return t.DialTLS(network, addr, tlsCfg)
|
||||
}
|
||||
|
||||
tlsCn, err := t.dialTLSWithContext(ctx, network, addr, tlsCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state := tlsCn.ConnectionState()
|
||||
if p := state.NegotiatedProtocol; p != NextProtoTLS {
|
||||
return nil, fmt.Errorf("http2: unexpected ALPN protocol %q; want %q", p, NextProtoTLS)
|
||||
}
|
||||
if !state.NegotiatedProtocolIsMutual {
|
||||
return nil, errors.New("http2: could not negotiate protocol mutually")
|
||||
}
|
||||
return tlsCn, nil
|
||||
}
|
||||
|
||||
// dialTLSWithContext uses tls.Dialer, added in Go 1.15, to open a TLS
|
||||
// connection.
|
||||
func (t *Transport) dialTLSWithContext(ctx context.Context, network, addr string, cfg *tls.Config) (*tls.Conn, error) {
|
||||
dialer := &tls.Dialer{
|
||||
Config: cfg,
|
||||
}
|
||||
cn, err := dialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsCn := cn.(*tls.Conn) // DialContext comment promises this will always succeed
|
||||
return tlsCn, nil
|
||||
}
|
||||
|
||||
// GoAwayError is returned by the Transport when the server closes the
|
||||
// TCP connection after sending a GOAWAY frame.
|
||||
type GoAwayError struct {
|
||||
LastStreamID uint32
|
||||
ErrCode ErrCode
|
||||
DebugData string
|
||||
}
|
||||
|
||||
func (e GoAwayError) Error() string {
|
||||
return fmt.Sprintf("http2: server sent GOAWAY and closed the connection; LastStreamID=%v, ErrCode=%v, debug=%q",
|
||||
e.LastStreamID, e.ErrCode, e.DebugData)
|
||||
}
|
||||
|
||||
// noCachedConnError is the concrete type of ErrNoCachedConn, which
|
||||
// needs to be detected by net/http regardless of whether it's its
|
||||
// bundled version (in h2_bundle.go with a rewritten type name) or
|
||||
// from a user's x/net/http2. As such, as it has a unique method name
|
||||
// (IsHTTP2NoCachedConnError) that net/http sniffs for via func
|
||||
// isNoCachedConnError.
|
||||
type noCachedConnError struct{}
|
||||
|
||||
func (noCachedConnError) IsHTTP2NoCachedConnError() {}
|
||||
func (noCachedConnError) Error() string { return "http2: no cached connection was available" }
|
||||
|
||||
// isNoCachedConnError reports whether err is of type noCachedConnError
|
||||
// or its equivalent renamed type in net/http2's h2_bundle.go. Both types
|
||||
// may coexist in the same running program.
|
||||
func isNoCachedConnError(err error) bool {
|
||||
_, ok := err.(interface{ IsHTTP2NoCachedConnError() })
|
||||
return ok
|
||||
}
|
||||
|
||||
var ErrNoCachedConn error = noCachedConnError{}
|
||||
+392
@@ -0,0 +1,392 @@
|
||||
// Copyright 2026 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.27 && !http2legacy
|
||||
|
||||
// Transport wrapping a net/http.Transport.
|
||||
|
||||
package http2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func configureTransport(t1 *http.Transport) error {
|
||||
_, err := configureTransports(t1)
|
||||
return err
|
||||
}
|
||||
|
||||
func configureTransports(t1 *http.Transport) (*Transport, error) {
|
||||
// ConfigureTransport returns an http2.Transport with a configuration
|
||||
// linked to the http.Transport's.
|
||||
tr2 := &Transport{}
|
||||
tr2.configure(t1)
|
||||
// Enable HTTP/2 on the transport, as the pre-wrapping implementation did:
|
||||
// net/http does not auto-enable it for a transport with a custom
|
||||
// TLSClientConfig or dialer.
|
||||
if t1.TLSClientConfig == nil {
|
||||
t1.TLSClientConfig = &tls.Config{}
|
||||
}
|
||||
if t1.Protocols == nil {
|
||||
t1.Protocols = new(http.Protocols)
|
||||
t1.Protocols.SetHTTP1(true)
|
||||
}
|
||||
t1.Protocols.SetHTTP2(true)
|
||||
return tr2, nil
|
||||
}
|
||||
|
||||
// transportConfig is passed to net/http.Transport.RegisterProtocol("http/2", config).
|
||||
// It provides the net/http.Transport with access to the configuration in the
|
||||
// x/net/http2.Transport.
|
||||
type transportConfig struct {
|
||||
t *Transport
|
||||
}
|
||||
|
||||
// Registered is called by net/http.Transport.RegisterProtocol,
|
||||
// to let us know that it understands the registration mechanism we're using.
|
||||
func (t transportConfig) Registered(t1 *http.Transport) {
|
||||
t.t.t1 = t1
|
||||
}
|
||||
|
||||
func (t transportConfig) DisableCompression() bool {
|
||||
return t.t.DisableCompression
|
||||
}
|
||||
|
||||
func (t transportConfig) MaxHeaderListSize() int64 {
|
||||
return int64(t.t.MaxHeaderListSize)
|
||||
}
|
||||
|
||||
func (t transportConfig) IdleConnTimeout() time.Duration {
|
||||
return t.t.IdleConnTimeout
|
||||
}
|
||||
|
||||
func (t transportConfig) HTTP2Config() http.HTTP2Config {
|
||||
return http.HTTP2Config{
|
||||
StrictMaxConcurrentRequests: t.t.StrictMaxConcurrentStreams,
|
||||
MaxDecoderHeaderTableSize: int(t.t.MaxDecoderHeaderTableSize),
|
||||
MaxEncoderHeaderTableSize: int(t.t.MaxEncoderHeaderTableSize),
|
||||
MaxReadFrameSize: int(t.t.MaxReadFrameSize),
|
||||
SendPingTimeout: t.t.ReadIdleTimeout,
|
||||
PingTimeout: t.t.PingTimeout,
|
||||
WriteByteTimeout: t.t.WriteByteTimeout,
|
||||
CountError: t.t.CountError,
|
||||
}
|
||||
}
|
||||
|
||||
// ExternalRoundTrip reports whether the Transport wants to take control of the RoundTrip call.
|
||||
// If the user hasn't configured a custom connection pool, we leave the RoundTrip up to net/http.
|
||||
func (t transportConfig) ExternalRoundTrip() bool {
|
||||
return t.t.ConnPool != nil
|
||||
}
|
||||
|
||||
// RoundTrip is used when the http.Transport is passing control of the full
|
||||
// RoundTrip to us--connection pooling, retries, etc.
|
||||
//
|
||||
// This is only used when the http2.Transport has a user-provided ConnPool.
|
||||
// Any other time, net/http handles everything.
|
||||
func (t transportConfig) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if t.t.ConnPool == nil {
|
||||
return nil, http.ErrSkipAltProtocol
|
||||
}
|
||||
return t.t.RoundTrip(req)
|
||||
}
|
||||
|
||||
// netConnContextKey passes a net.Conn to http.Transport.NewClientConn.
|
||||
// See http2.Transport.NewClientConn.
|
||||
type netConnContextKey struct{}
|
||||
|
||||
// ConnFromContext lets the http.Transport fetch a net.Conn out of a context
|
||||
// passed to NewClientConn. See http2.Transport.NewClientConn.
|
||||
func (t transportConfig) ConnFromContext(ctx context.Context) net.Conn {
|
||||
nc, _ := ctx.Value(netConnContextKey{}).(net.Conn)
|
||||
return nc
|
||||
}
|
||||
|
||||
// http2TransportContextKey marks a RoundTrip as needing its dial handled by the http2.Transport.
|
||||
// We set this for http2.RoundTrip calls, where the historical behavior is to use the
|
||||
// http2.Transport's dialer.
|
||||
type http2TransportContextKey struct{}
|
||||
|
||||
// DialFromContext dials a new connection using the http2.Transport's DialTLS/DialTLSContext.
|
||||
func (t transportConfig) DialFromContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
if ctx.Value(http2TransportContextKey{}) == nil {
|
||||
// We're being called from a RoundTrip that did not start with an http2.Transport.
|
||||
// Use the http.Transport's dialer.
|
||||
return nil, errors.ErrUnsupported
|
||||
}
|
||||
|
||||
tlsConf := t.t.TLSClientConfig
|
||||
if tlsConf == nil {
|
||||
tlsConf = &tls.Config{}
|
||||
} else {
|
||||
tlsConf = tlsConf.Clone()
|
||||
}
|
||||
if !slices.Contains(tlsConf.NextProtos, "h2") {
|
||||
tlsConf.NextProtos = append([]string{"h2"}, tlsConf.NextProtos...)
|
||||
}
|
||||
if tlsConf.ServerName == "" {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err == nil {
|
||||
tlsConf.ServerName = host
|
||||
}
|
||||
}
|
||||
return t.t.dialTLS(ctx, network, address, tlsConf)
|
||||
}
|
||||
|
||||
type transportInternal struct {
|
||||
initOnce sync.Once
|
||||
t1 *http.Transport
|
||||
}
|
||||
|
||||
func (t *Transport) init() {
|
||||
t.initOnce.Do(func() {
|
||||
if t.t1 != nil {
|
||||
return
|
||||
}
|
||||
t1 := &http.Transport{}
|
||||
t.configure(t1)
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Transport) configure(t1 *http.Transport) {
|
||||
t1.RegisterProtocol("http/2", transportConfig{t})
|
||||
// tr2.t1 is set by transportConfig.Registered.
|
||||
if t.t1 != t1 {
|
||||
panic("http2: net/http does not support this version of x/net/http2")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) roundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
|
||||
t.init()
|
||||
|
||||
if req.URL.Scheme == "http" && !t.AllowHTTP {
|
||||
return nil, errors.New("http2: unencrypted HTTP/2 not enabled")
|
||||
}
|
||||
|
||||
// When the Transport has a user-provided connection pool (unusual, deprecated),
|
||||
// we need to handle picking a connection, retrys, etc.
|
||||
if t.ConnPool != nil {
|
||||
return t.roundTripViaPool(req, opt, t.ConnPool)
|
||||
}
|
||||
|
||||
// Setting this context key lets net/http know that if it is necessary to dial
|
||||
// a new connection, we should handle the net.Dial.
|
||||
//
|
||||
// Both http.Transport and http2.Transport allow the user to provide a custom
|
||||
// dial function, and historically you only get the dial function from the
|
||||
// Transport you're calling RoundTrip on.
|
||||
ctx := context.WithValue(req.Context(), http2TransportContextKey{}, t)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
return t.t1.RoundTrip(req)
|
||||
}
|
||||
|
||||
func (t *Transport) closeIdleConnections() {
|
||||
t.init()
|
||||
t.t1.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (t *Transport) newUserClientConn(c net.Conn) (*ClientConn, error) {
|
||||
// http.Transport's NewClientConn doesn't provide a supported way to create
|
||||
// a connection from a net.Conn. (This might be useful to add in the future?)
|
||||
// We're going to craftily sneak one in via the context key, with the
|
||||
// scheme of "http/2" telling NewClientConn to look for it.
|
||||
ctx := context.WithValue(context.Background(), netConnContextKey{}, c)
|
||||
|
||||
nhcc, err := t.t1.NewClientConn(ctx, "http/2", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cc := &ClientConn{cc: nhcc, tr: t, tconn: c}
|
||||
nhcc.SetStateHook(cc.stateHook)
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
// ClientConn is the state of a single HTTP/2 client connection to an
|
||||
// HTTP/2 server.
|
||||
type ClientConn struct {
|
||||
cc *http.ClientConn
|
||||
tconn net.Conn
|
||||
tr *Transport
|
||||
doNotReuse bool
|
||||
|
||||
mu sync.Mutex
|
||||
closing bool
|
||||
closed bool
|
||||
roundTrips int
|
||||
reserved int
|
||||
starting int
|
||||
pending int
|
||||
maxConcurrent int
|
||||
lastIdle time.Time
|
||||
shutdownc chan struct{}
|
||||
|
||||
atomicReused uint32 // whether conn is being reused; atomic
|
||||
}
|
||||
|
||||
func (cc *ClientConn) roundTrip(req *http.Request) (*http.Response, error) {
|
||||
err := func() error {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
if cc.doNotReuse {
|
||||
return errClientConnUnusable
|
||||
}
|
||||
cc.roundTrips++
|
||||
if cc.reserved > 0 {
|
||||
// We've already reserved a concurrency slot for this request.
|
||||
cc.reserved--
|
||||
} else if cc.cc.Reserve() != nil {
|
||||
// We don't seem to have an available concurrency slot,
|
||||
// so bump the pending count (requests waiting for a slot).
|
||||
cc.pending++
|
||||
}
|
||||
// ClientConn.Shutdown will not shut down the conn while
|
||||
// cc.starting > 0 or cc.cc.InFlight() > 0.
|
||||
//
|
||||
// The starting state covers the gap between us deciding to
|
||||
// start sending the request, and actually sending it.
|
||||
cc.starting++
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := cc.cc.RoundTrip(req)
|
||||
cc.mu.Lock()
|
||||
cc.starting--
|
||||
if cc.pending > 0 {
|
||||
// A request completing frees up a concurrency slot for
|
||||
// a pending request to start.
|
||||
cc.pending--
|
||||
}
|
||||
cc.updateStateLocked()
|
||||
cc.mu.Unlock()
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (cc *ClientConn) canTakeNewRequest() bool {
|
||||
return cc.cc.Available() > 0 && !cc.doNotReuse
|
||||
}
|
||||
|
||||
func (cc *ClientConn) close() error {
|
||||
return cc.cc.Close()
|
||||
}
|
||||
|
||||
func (cc *ClientConn) ping(ctx context.Context) error {
|
||||
// Ask net/http to ping its connection by sending a request with a method of ":ping".
|
||||
_, err := cc.cc.RoundTrip((&http.Request{
|
||||
Method: ":ping",
|
||||
}).WithContext(ctx))
|
||||
return err
|
||||
}
|
||||
|
||||
func (cc *ClientConn) reserveNewRequest() bool {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
if cc.doNotReuse {
|
||||
return false
|
||||
}
|
||||
if err := cc.cc.Reserve(); err != nil {
|
||||
return false
|
||||
}
|
||||
cc.reserved++
|
||||
return true
|
||||
}
|
||||
|
||||
func (cc *ClientConn) setDoNotReuse() {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
cc.doNotReuse = true
|
||||
cc.closing = true
|
||||
}
|
||||
|
||||
func (cc *ClientConn) shutdown(ctx context.Context) error {
|
||||
cc.mu.Lock()
|
||||
inFlight := cc.cc.InFlight() + cc.starting
|
||||
if inFlight > 0 && cc.shutdownc == nil {
|
||||
cc.shutdownc = make(chan struct{})
|
||||
}
|
||||
shutdownc := cc.shutdownc
|
||||
cc.mu.Unlock()
|
||||
if shutdownc != nil {
|
||||
// Wait for in-flight requests to finish.
|
||||
select {
|
||||
case <-shutdownc:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
cc.cc.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc *ClientConn) state() ClientConnState {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
cc.updateStateLocked()
|
||||
return ClientConnState{
|
||||
Closed: cc.closed,
|
||||
Closing: cc.closing,
|
||||
StreamsActive: cc.cc.InFlight() - cc.reserved,
|
||||
StreamsReserved: cc.reserved,
|
||||
StreamsPending: cc.pending,
|
||||
MaxConcurrentStreams: uint32(min(int64(cc.maxConcurrent), math.MaxUint32)),
|
||||
LastIdle: cc.lastIdle,
|
||||
}
|
||||
}
|
||||
|
||||
// stateHook is the http.ClientConn's state hook.
|
||||
func (cc *ClientConn) stateHook(*http.ClientConn) {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
cc.updateStateLocked()
|
||||
}
|
||||
|
||||
func (cc *ClientConn) updateStateLocked() {
|
||||
if cc.cc.Err() != nil && !cc.closed {
|
||||
cc.closing = true
|
||||
cc.closed = true
|
||||
if cc.tr.ConnPool != nil {
|
||||
// Do the ConnPool update in another goroutine,
|
||||
// to avoid holding the conn mutex while it runs.
|
||||
go cc.tr.ConnPool.MarkDead(cc)
|
||||
}
|
||||
}
|
||||
if cc.cc.InFlight() == 0 && cc.roundTrips > 0 && cc.starting == 0 {
|
||||
cc.lastIdle = time.Now()
|
||||
}
|
||||
if !cc.closed {
|
||||
// This is slightly racy (a request could start or finish in between
|
||||
// the Available and InFlight calls), but the best we can do given that
|
||||
// the net/http ClientConn API doesn't expose the conn's max concurrency.
|
||||
cc.maxConcurrent = cc.cc.Available() + cc.cc.InFlight()
|
||||
}
|
||||
if cc.shutdownc != nil && cc.cc.InFlight()+cc.starting == 0 {
|
||||
close(cc.shutdownc)
|
||||
cc.shutdownc = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *ClientConn) stopIdleTimer() {}
|
||||
|
||||
// traceGotConn is (when http2legacy is not enabled) only used for tracing
|
||||
// connections acquired while using a user-provided ClientConnPool.
|
||||
func traceGotConn(req *http.Request, cc *ClientConn, reused bool) {
|
||||
trace := httptrace.ContextClientTrace(req.Context())
|
||||
if trace == nil || trace.GotConn == nil {
|
||||
return
|
||||
}
|
||||
ci := httptrace.GotConnInfo{Conn: cc.tconn}
|
||||
ci.Reused = reused
|
||||
trace.GotConn(ci)
|
||||
}
|
||||
+2
-44
@@ -2,54 +2,12 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !(go1.27 && !http2legacy)
|
||||
|
||||
package http2
|
||||
|
||||
import "fmt"
|
||||
|
||||
// WriteScheduler is the interface implemented by HTTP/2 write schedulers.
|
||||
// Methods are never called concurrently.
|
||||
//
|
||||
// Deprecated: User-provided write schedulers are deprecated.
|
||||
type WriteScheduler interface {
|
||||
// OpenStream opens a new stream in the write scheduler.
|
||||
// It is illegal to call this with streamID=0 or with a streamID that is
|
||||
// already open -- the call may panic.
|
||||
OpenStream(streamID uint32, options OpenStreamOptions)
|
||||
|
||||
// CloseStream closes a stream in the write scheduler. Any frames queued on
|
||||
// this stream should be discarded. It is illegal to call this on a stream
|
||||
// that is not open -- the call may panic.
|
||||
CloseStream(streamID uint32)
|
||||
|
||||
// AdjustStream adjusts the priority of the given stream. This may be called
|
||||
// on a stream that has not yet been opened or has been closed. Note that
|
||||
// RFC 7540 allows PRIORITY frames to be sent on streams in any state. See:
|
||||
// https://tools.ietf.org/html/rfc7540#section-5.1
|
||||
AdjustStream(streamID uint32, priority PriorityParam)
|
||||
|
||||
// Push queues a frame in the scheduler. In most cases, this will not be
|
||||
// called with wr.StreamID()!=0 unless that stream is currently open. The one
|
||||
// exception is RST_STREAM frames, which may be sent on idle or closed streams.
|
||||
Push(wr FrameWriteRequest)
|
||||
|
||||
// Pop dequeues the next frame to write. Returns false if no frames can
|
||||
// be written. Frames with a given wr.StreamID() are Pop'd in the same
|
||||
// order they are Push'd, except RST_STREAM frames. No frames should be
|
||||
// discarded except by CloseStream.
|
||||
Pop() (wr FrameWriteRequest, ok bool)
|
||||
}
|
||||
|
||||
// OpenStreamOptions specifies extra options for WriteScheduler.OpenStream.
|
||||
//
|
||||
// Deprecated: User-provided write schedulers are deprecated.
|
||||
type OpenStreamOptions struct {
|
||||
// PusherID is zero if the stream was initiated by the client. Otherwise,
|
||||
// PusherID names the stream that pushed the newly opened stream.
|
||||
PusherID uint32
|
||||
// priority is used to set the priority of the newly opened stream.
|
||||
priority PriorityParam
|
||||
}
|
||||
|
||||
// FrameWriteRequest is a request to write a frame.
|
||||
//
|
||||
// Deprecated: User-provided write schedulers are deprecated.
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
// Copyright 2026 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package http2
|
||||
|
||||
// WriteScheduler is the interface implemented by HTTP/2 write schedulers.
|
||||
// Methods are never called concurrently.
|
||||
//
|
||||
// Deprecated: User-provided write schedulers are deprecated.
|
||||
type WriteScheduler interface {
|
||||
// OpenStream opens a new stream in the write scheduler.
|
||||
// It is illegal to call this with streamID=0 or with a streamID that is
|
||||
// already open -- the call may panic.
|
||||
OpenStream(streamID uint32, options OpenStreamOptions)
|
||||
|
||||
// CloseStream closes a stream in the write scheduler. Any frames queued on
|
||||
// this stream should be discarded. It is illegal to call this on a stream
|
||||
// that is not open -- the call may panic.
|
||||
CloseStream(streamID uint32)
|
||||
|
||||
// AdjustStream adjusts the priority of the given stream. This may be called
|
||||
// on a stream that has not yet been opened or has been closed. Note that
|
||||
// RFC 7540 allows PRIORITY frames to be sent on streams in any state. See:
|
||||
// https://tools.ietf.org/html/rfc7540#section-5.1
|
||||
AdjustStream(streamID uint32, priority PriorityParam)
|
||||
|
||||
// Push queues a frame in the scheduler. In most cases, this will not be
|
||||
// called with wr.StreamID()!=0 unless that stream is currently open. The one
|
||||
// exception is RST_STREAM frames, which may be sent on idle or closed streams.
|
||||
Push(wr FrameWriteRequest)
|
||||
|
||||
// Pop dequeues the next frame to write. Returns false if no frames can
|
||||
// be written. Frames with a given wr.StreamID() are Pop'd in the same
|
||||
// order they are Push'd, except RST_STREAM frames. No frames should be
|
||||
// discarded except by CloseStream.
|
||||
Pop() (wr FrameWriteRequest, ok bool)
|
||||
}
|
||||
|
||||
// OpenStreamOptions specifies extra options for WriteScheduler.OpenStream.
|
||||
//
|
||||
// Deprecated: User-provided write schedulers are deprecated.
|
||||
type OpenStreamOptions struct {
|
||||
// PusherID is zero if the stream was initiated by the client. Otherwise,
|
||||
// PusherID names the stream that pushed the newly opened stream.
|
||||
PusherID uint32
|
||||
// priority is used to set the priority of the newly opened stream.
|
||||
priority PriorityParam
|
||||
}
|
||||
|
||||
// PriorityWriteSchedulerConfig configures a priorityWriteScheduler.
|
||||
//
|
||||
// Deprecated: User-provided write schedulers are deprecated.
|
||||
type PriorityWriteSchedulerConfig struct {
|
||||
// MaxClosedNodesInTree controls the maximum number of closed streams to
|
||||
// retain in the priority tree. Setting this to zero saves a small amount
|
||||
// of memory at the cost of performance.
|
||||
//
|
||||
// See RFC 7540, Section 5.3.4:
|
||||
// "It is possible for a stream to become closed while prioritization
|
||||
// information ... is in transit. ... This potentially creates suboptimal
|
||||
// prioritization, since the stream could be given a priority that is
|
||||
// different from what is intended. To avoid these problems, an endpoint
|
||||
// SHOULD retain stream prioritization state for a period after streams
|
||||
// become closed. The longer state is retained, the lower the chance that
|
||||
// streams are assigned incorrect or default priority values."
|
||||
MaxClosedNodesInTree int
|
||||
|
||||
// MaxIdleNodesInTree controls the maximum number of idle streams to
|
||||
// retain in the priority tree. Setting this to zero saves a small amount
|
||||
// of memory at the cost of performance.
|
||||
//
|
||||
// See RFC 7540, Section 5.3.4:
|
||||
// Similarly, streams that are in the "idle" state can be assigned
|
||||
// priority or become a parent of other streams. This allows for the
|
||||
// creation of a grouping node in the dependency tree, which enables
|
||||
// more flexible expressions of priority. Idle streams begin with a
|
||||
// default priority (Section 5.3.5).
|
||||
MaxIdleNodesInTree int
|
||||
|
||||
// ThrottleOutOfOrderWrites enables write throttling to help ensure that
|
||||
// data is delivered in priority order. This works around a race where
|
||||
// stream B depends on stream A and both streams are about to call Write
|
||||
// to queue DATA frames. If B wins the race, a naive scheduler would eagerly
|
||||
// write as much data from B as possible, but this is suboptimal because A
|
||||
// is a higher-priority stream. With throttling enabled, we write a small
|
||||
// amount of data from B to minimize the amount of bandwidth that B can
|
||||
// steal from A.
|
||||
ThrottleOutOfOrderWrites bool
|
||||
}
|
||||
+2
-41
@@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !(go1.27 && !http2legacy)
|
||||
|
||||
package http2
|
||||
|
||||
import (
|
||||
@@ -13,47 +15,6 @@ import (
|
||||
// RFC 7540, Section 5.3.5: the default weight is 16.
|
||||
const priorityDefaultWeightRFC7540 = 15 // 16 = 15 + 1
|
||||
|
||||
// PriorityWriteSchedulerConfig configures a priorityWriteScheduler.
|
||||
//
|
||||
// Deprecated: User-provided write schedulers are deprecated.
|
||||
type PriorityWriteSchedulerConfig struct {
|
||||
// MaxClosedNodesInTree controls the maximum number of closed streams to
|
||||
// retain in the priority tree. Setting this to zero saves a small amount
|
||||
// of memory at the cost of performance.
|
||||
//
|
||||
// See RFC 7540, Section 5.3.4:
|
||||
// "It is possible for a stream to become closed while prioritization
|
||||
// information ... is in transit. ... This potentially creates suboptimal
|
||||
// prioritization, since the stream could be given a priority that is
|
||||
// different from what is intended. To avoid these problems, an endpoint
|
||||
// SHOULD retain stream prioritization state for a period after streams
|
||||
// become closed. The longer state is retained, the lower the chance that
|
||||
// streams are assigned incorrect or default priority values."
|
||||
MaxClosedNodesInTree int
|
||||
|
||||
// MaxIdleNodesInTree controls the maximum number of idle streams to
|
||||
// retain in the priority tree. Setting this to zero saves a small amount
|
||||
// of memory at the cost of performance.
|
||||
//
|
||||
// See RFC 7540, Section 5.3.4:
|
||||
// Similarly, streams that are in the "idle" state can be assigned
|
||||
// priority or become a parent of other streams. This allows for the
|
||||
// creation of a grouping node in the dependency tree, which enables
|
||||
// more flexible expressions of priority. Idle streams begin with a
|
||||
// default priority (Section 5.3.5).
|
||||
MaxIdleNodesInTree int
|
||||
|
||||
// ThrottleOutOfOrderWrites enables write throttling to help ensure that
|
||||
// data is delivered in priority order. This works around a race where
|
||||
// stream B depends on stream A and both streams are about to call Write
|
||||
// to queue DATA frames. If B wins the race, a naive scheduler would eagerly
|
||||
// write as much data from B as possible, but this is suboptimal because A
|
||||
// is a higher-priority stream. With throttling enabled, we write a small
|
||||
// amount of data from B to minimize the amount of bandwidth that B can
|
||||
// steal from A.
|
||||
ThrottleOutOfOrderWrites bool
|
||||
}
|
||||
|
||||
// NewPriorityWriteScheduler constructs a WriteScheduler that schedules
|
||||
// frames by following HTTP/2 priorities as described in RFC 7540 Section 5.3.
|
||||
// If cfg is nil, default options are used.
|
||||
|
||||
+2
@@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !(go1.27 && !http2legacy)
|
||||
|
||||
package http2
|
||||
|
||||
import (
|
||||
|
||||
+2
@@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !(go1.27 && !http2legacy)
|
||||
|
||||
package http2
|
||||
|
||||
import "math"
|
||||
|
||||
+2
@@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !(go1.27 && !http2legacy)
|
||||
|
||||
package http2
|
||||
|
||||
import (
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
|
||||
|
||||
// Copyright 2021 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.
|
||||
|
||||
//go:build go1.18
|
||||
|
||||
package idna
|
||||
|
||||
// Transitional processing is disabled by default in Go 1.18.
|
||||
// https://golang.org/issue/47510
|
||||
const transitionalLookup = false
|
||||
Generated
Vendored
+148
-37
@@ -4,8 +4,6 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.10
|
||||
|
||||
// Package idna implements IDNA2008 using the compatibility processing
|
||||
// defined by UTS (Unicode Technical Standard) #46, which defines a standard to
|
||||
// deal with the transition from IDNA2003.
|
||||
@@ -20,6 +18,7 @@ package idna // import "golang.org/x/net/idna"
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/secure/bidirule"
|
||||
@@ -27,6 +26,8 @@ import (
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
const unicode16 = unicode.Version >= "16.0.0"
|
||||
|
||||
// NOTE: Unlike common practice in Go APIs, the functions will return a
|
||||
// sanitized domain name in case of errors. Browsers sometimes use a partially
|
||||
// evaluated string as lookup.
|
||||
@@ -101,6 +102,11 @@ func ValidateLabels(enable bool) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// validateLabels reports whether the ValidateLabels option is enabled.
|
||||
func (p *Profile) validateLabels() bool {
|
||||
return p.fromPuny != nil
|
||||
}
|
||||
|
||||
// CheckHyphens sets whether to check for correct use of hyphens ('-') in
|
||||
// labels. Most web browsers do not have this option set, since labels such as
|
||||
// "r3---sn-apo3qvuoxuxbt-j5pe" are in common use.
|
||||
@@ -263,6 +269,10 @@ func (p *Profile) String() string {
|
||||
return s
|
||||
}
|
||||
|
||||
// Transitional processing is disabled by default as of Go 1.18.
|
||||
// https://golang.org/issue/47510
|
||||
const transitionalLookup = false
|
||||
|
||||
var (
|
||||
// Punycode is a Profile that does raw punycode processing with a minimum
|
||||
// of validation.
|
||||
@@ -324,15 +334,30 @@ func (e labelError) Error() string {
|
||||
return fmt.Sprintf("idna: invalid label %q", e.label)
|
||||
}
|
||||
|
||||
type runeError rune
|
||||
|
||||
func (e runeError) code() string { return "P1" }
|
||||
func (e runeError) Error() string {
|
||||
return fmt.Sprintf("idna: disallowed rune %U", e)
|
||||
type runeError struct {
|
||||
r rune
|
||||
code_ string
|
||||
}
|
||||
|
||||
// process implements the algorithm described in section 4 of UTS #46,
|
||||
// see https://www.unicode.org/reports/tr46.
|
||||
func (e runeError) code() string { return e.code_ }
|
||||
func (e runeError) Error() string {
|
||||
return fmt.Sprintf("idna: disallowed rune %U", e.r)
|
||||
}
|
||||
|
||||
// code16 returns old for Unicode < 16, new for Unicode >= 16.
|
||||
func code16(old, new string) string {
|
||||
if unicode16 {
|
||||
return new
|
||||
}
|
||||
return old
|
||||
}
|
||||
|
||||
// process10 implements the algorithm described in section 4 of UTS #46.
|
||||
// It implements both the Unicode 10 algorithm
|
||||
// (https://www.unicode.org/reports/tr46/tr46-19.html)
|
||||
// and the Unicode 16 algorithm
|
||||
// (https://www.unicode.org/reports/tr46/tr46-35.html)
|
||||
// depending on unicode16, which in turn depends on unicode.Version.
|
||||
func (p *Profile) process(s string, toASCII bool) (string, error) {
|
||||
var err error
|
||||
var isBidi bool
|
||||
@@ -347,8 +372,12 @@ func (p *Profile) process(s string, toASCII bool) (string, error) {
|
||||
// TODO: allow for a quick check of the tables data.
|
||||
// It seems like we should only create this error on ToASCII, but the
|
||||
// UTS 46 conformance tests suggests we should always check this.
|
||||
labelCode := "X4_2"
|
||||
if !unicode16 || toASCII {
|
||||
labelCode = "A4"
|
||||
}
|
||||
if err == nil && p.verifyDNSLength && s == "" {
|
||||
err = &labelError{s, "A4"}
|
||||
err = labelError{s, labelCode}
|
||||
}
|
||||
labels := labelIter{orig: s}
|
||||
for ; !labels.done(); labels.next() {
|
||||
@@ -357,12 +386,13 @@ func (p *Profile) process(s string, toASCII bool) (string, error) {
|
||||
// Empty labels are not okay. The label iterator skips the last
|
||||
// label if it is empty.
|
||||
if err == nil && p.verifyDNSLength {
|
||||
err = &labelError{s, "A4"}
|
||||
err = labelError{s, labelCode}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(label, acePrefix) {
|
||||
u, err2 := decode(label[len(acePrefix):])
|
||||
enc := label[len(acePrefix):]
|
||||
u, err2 := decode(enc)
|
||||
if err2 != nil {
|
||||
if err == nil {
|
||||
err = err2
|
||||
@@ -370,6 +400,9 @@ func (p *Profile) process(s string, toASCII bool) (string, error) {
|
||||
// Spec says keep the old label.
|
||||
continue
|
||||
}
|
||||
if unicode16 && err == nil && len(u) > 0 && isASCII(u) {
|
||||
err = punyError(enc)
|
||||
}
|
||||
isBidi = isBidi || bidirule.DirectionString(u) != bidi.LeftToRight
|
||||
labels.set(u)
|
||||
if err == nil && p.fromPuny != nil {
|
||||
@@ -379,16 +412,16 @@ func (p *Profile) process(s string, toASCII bool) (string, error) {
|
||||
// This should be called on NonTransitional, according to the
|
||||
// spec, but that currently does not have any effect. Use the
|
||||
// original profile to preserve options.
|
||||
err = p.validateLabel(u)
|
||||
err = p.validateLabel(u, labelCode)
|
||||
}
|
||||
} else if err == nil {
|
||||
err = p.validateLabel(label)
|
||||
err = p.validateLabel(label, labelCode)
|
||||
}
|
||||
}
|
||||
if isBidi && p.bidirule != nil && err == nil {
|
||||
for labels.reset(); !labels.done(); labels.next() {
|
||||
if !p.bidirule(labels.label()) {
|
||||
err = &labelError{s, "B"}
|
||||
err = labelError{s, "B"}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -406,24 +439,36 @@ func (p *Profile) process(s string, toASCII bool) (string, error) {
|
||||
}
|
||||
n := len(label)
|
||||
if p.verifyDNSLength && err == nil && (n == 0 || n > 63) {
|
||||
err = &labelError{label, "A4"}
|
||||
err = labelError{label, labelCode}
|
||||
}
|
||||
}
|
||||
}
|
||||
s = labels.result()
|
||||
if toASCII && p.verifyDNSLength && err == nil {
|
||||
if unicode16 && strings.HasSuffix(s, ".") {
|
||||
err = labelError{s, labelCode}
|
||||
}
|
||||
// Compute the length of the domain name minus the root label and its dot.
|
||||
n := len(s)
|
||||
if n > 0 && s[n-1] == '.' {
|
||||
n--
|
||||
}
|
||||
if len(s) < 1 || n > 253 {
|
||||
err = &labelError{s, "A4"}
|
||||
err = labelError{s, labelCode}
|
||||
}
|
||||
}
|
||||
return s, err
|
||||
}
|
||||
|
||||
func isASCII(s string) bool {
|
||||
for _, c := range []byte(s) {
|
||||
if c >= 0x80 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func normalize(p *Profile, s string) (mapped string, isBidi bool, err error) {
|
||||
// TODO: consider first doing a quick check to see if any of these checks
|
||||
// need to be done. This will make it slower in the general case, but
|
||||
@@ -436,12 +481,12 @@ func normalize(p *Profile, s string) (mapped string, isBidi bool, err error) {
|
||||
func validateRegistration(p *Profile, s string) (idem string, bidi bool, err error) {
|
||||
// TODO: filter need for normalization in loop below.
|
||||
if !norm.NFC.IsNormalString(s) {
|
||||
return s, false, &labelError{s, "V1"}
|
||||
return s, false, labelError{s, "V1"}
|
||||
}
|
||||
for i := 0; i < len(s); {
|
||||
v, sz := trie.lookupString(s[i:])
|
||||
if sz == 0 {
|
||||
return s, bidi, runeError(utf8.RuneError)
|
||||
return s, bidi, runeError{utf8.RuneError, "P1"}
|
||||
}
|
||||
bidi = bidi || info(v).isBidi(s[i:])
|
||||
// Copy bytes not copied so far.
|
||||
@@ -449,9 +494,12 @@ func validateRegistration(p *Profile, s string) (idem string, bidi bool, err err
|
||||
// TODO: handle the NV8 defined in the Unicode idna data set to allow
|
||||
// for strict conformance to IDNA2008.
|
||||
case valid, deviation:
|
||||
if sz == 1 && p.useSTD3Rules && !allowedSTD3(rune(s[i])) {
|
||||
return s, bidi, runeError{rune(s[i]), "P1"}
|
||||
}
|
||||
case disallowed, mapped, unknown, ignored:
|
||||
r, _ := utf8.DecodeRuneInString(s[i:])
|
||||
return s, bidi, runeError(r)
|
||||
return s, bidi, runeError{r, "P1"}
|
||||
}
|
||||
i += sz
|
||||
}
|
||||
@@ -489,7 +537,7 @@ func validateAndMap(p *Profile, s string) (vm string, bidi bool, err error) {
|
||||
b = append(b, "\ufffd"...)
|
||||
k = len(s)
|
||||
if err == nil {
|
||||
err = runeError(utf8.RuneError)
|
||||
err = runeError{utf8.RuneError, "P1"}
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -502,14 +550,26 @@ func validateAndMap(p *Profile, s string) (vm string, bidi bool, err error) {
|
||||
case valid:
|
||||
continue
|
||||
case disallowed:
|
||||
if err == nil {
|
||||
// Unicode 16 delays the error until validateLabels.
|
||||
// Unicode 10 gave an error now.
|
||||
if !unicode16 && err == nil {
|
||||
r, _ := utf8.DecodeRuneInString(s[start:])
|
||||
err = runeError(r)
|
||||
err = runeError{r, "P1"}
|
||||
}
|
||||
continue
|
||||
case mapped, deviation:
|
||||
case deviation:
|
||||
if unicode16 && !p.transitional {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
case mapped:
|
||||
b = append(b, s[k:start]...)
|
||||
b = info(v).appendMapping(b, s[start:i])
|
||||
// Unicode 16 requires a special case to handle ẞ -> ss in transitional mode.
|
||||
if unicode16 && p.transitional && s[start:start+sz] == "ẞ" {
|
||||
b = append(b, "ss"...)
|
||||
} else {
|
||||
b = info(v).appendMapping(b, s[start:i])
|
||||
}
|
||||
case ignored:
|
||||
b = append(b, s[k:start]...)
|
||||
// drop the rune
|
||||
@@ -600,13 +660,13 @@ const acePrefix = "xn--"
|
||||
|
||||
func (p *Profile) simplify(cat category) category {
|
||||
switch cat {
|
||||
case disallowedSTD3Mapped:
|
||||
case disallowedSTD3Mapped: // only happens for pre-Unicode 16
|
||||
if p.useSTD3Rules {
|
||||
cat = disallowed
|
||||
} else {
|
||||
cat = mapped
|
||||
}
|
||||
case disallowedSTD3Valid:
|
||||
case disallowedSTD3Valid: // only happens for pre-Unicode 16
|
||||
if p.useSTD3Rules {
|
||||
cat = disallowed
|
||||
} else {
|
||||
@@ -625,17 +685,18 @@ func (p *Profile) simplify(cat category) category {
|
||||
|
||||
func validateFromPunycode(p *Profile, s string) error {
|
||||
if !norm.NFC.IsNormalString(s) {
|
||||
return &labelError{s, "V1"}
|
||||
return labelError{s, "V1"}
|
||||
}
|
||||
// TODO: detect whether string may have to be normalized in the following
|
||||
// loop.
|
||||
for i := 0; i < len(s); {
|
||||
v, sz := trie.lookupString(s[i:])
|
||||
if sz == 0 {
|
||||
return runeError(utf8.RuneError)
|
||||
return runeError{utf8.RuneError, "P1"}
|
||||
}
|
||||
if c := p.simplify(info(v).category()); c != valid && c != deviation {
|
||||
return &labelError{s, "V6"}
|
||||
cat := info(v).category()
|
||||
if c := p.simplify(cat); c != valid && c != deviation {
|
||||
return labelError{s, code16("V6", "V7")}
|
||||
}
|
||||
i += sz
|
||||
}
|
||||
@@ -704,23 +765,51 @@ var joinStates = [][numJoinTypes]joinState{
|
||||
},
|
||||
}
|
||||
|
||||
// allowedSTD3 reports whether r is a rune that can appear in a domain name
|
||||
// according to STD3. We allow all non-ASCII runes and then letters, digits, hyphens.
|
||||
// We also add dot so that this can be run against the whole name and not just
|
||||
// a single name element (label). The surrounding code checks dots well enough.
|
||||
func allowedSTD3(r rune) bool {
|
||||
return r >= 0x80 || 'a' <= r && r <= 'z' || '0' <= r && r <= '9' || r == '-' || r == '.'
|
||||
}
|
||||
|
||||
// validateLabel validates the criteria from Section 4.1. Item 1, 4, and 6 are
|
||||
// already implicitly satisfied by the overall implementation.
|
||||
func (p *Profile) validateLabel(s string) (err error) {
|
||||
func (p *Profile) validateLabel(s string, labelCode string) (err error) {
|
||||
if s == "" {
|
||||
if p.verifyDNSLength {
|
||||
return &labelError{s, "A4"}
|
||||
return labelError{s, labelCode}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if p.checkHyphens {
|
||||
if len(s) > 4 && s[2] == '-' && s[3] == '-' {
|
||||
return &labelError{s, "V2"}
|
||||
return labelError{s, "V2"}
|
||||
}
|
||||
if s[0] == '-' || s[len(s)-1] == '-' {
|
||||
return &labelError{s, "V3"}
|
||||
return labelError{s, "V3"}
|
||||
}
|
||||
}
|
||||
|
||||
// Unicode 16's TR 46 delays the rune validity checks until after the label is decoded.
|
||||
// (validateAndMap did not reject them earlier.)
|
||||
if unicode16 && p.validateLabels() {
|
||||
for i := 0; i < len(s); {
|
||||
v, sz := trie.lookupString(s[i:])
|
||||
if sz == 0 {
|
||||
return runeError{utf8.RuneError, "P1"}
|
||||
}
|
||||
cat := info(v).category()
|
||||
if c := p.simplify(cat); c != valid && (!p.transitional || c != deviation) {
|
||||
return labelError{s, "V7"}
|
||||
}
|
||||
if sz == 1 && p.useSTD3Rules && !allowedSTD3(rune(s[i])) {
|
||||
return runeError{rune(s[i]), "U1"}
|
||||
}
|
||||
i += sz
|
||||
}
|
||||
}
|
||||
|
||||
if !p.checkJoiners {
|
||||
return nil
|
||||
}
|
||||
@@ -729,7 +818,7 @@ func (p *Profile) validateLabel(s string) (err error) {
|
||||
v, sz := trie.lookupString(s)
|
||||
x := info(v)
|
||||
if x.isModifier() {
|
||||
return &labelError{s, "V5"}
|
||||
return labelError{s, code16("V5", "V6")}
|
||||
}
|
||||
// Quickly return in the absence of zero-width (non) joiners.
|
||||
if strings.Index(s, zwj) == -1 && strings.Index(s, zwnj) == -1 {
|
||||
@@ -754,8 +843,9 @@ func (p *Profile) validateLabel(s string) (err error) {
|
||||
x = info(v)
|
||||
}
|
||||
if st == stateFAIL || st == stateAfter {
|
||||
return &labelError{s, "C"}
|
||||
return labelError{s, "C"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -767,3 +857,24 @@ func ascii(s string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// appendMapping appends the mapping for the respective rune. isMapped must be
|
||||
// true. A mapping is a categorization of a rune as defined in UTS #46.
|
||||
func (c info) appendMapping(b []byte, s string) []byte {
|
||||
index := int(c >> indexShift)
|
||||
if c&xorBit == 0 {
|
||||
p := index
|
||||
return append(b, mappings[mappingIndex[p]:mappingIndex[p+1]]...)
|
||||
}
|
||||
b = append(b, s...)
|
||||
if c&inlineXOR == inlineXOR {
|
||||
// TODO: support and handle two-byte inline masks
|
||||
b[len(b)-1] ^= byte(index)
|
||||
} else {
|
||||
for p := len(b) - int(xorData[index]); p < len(b); p++ {
|
||||
index++
|
||||
b[p] ^= xorData[index]
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
-717
@@ -1,717 +0,0 @@
|
||||
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
|
||||
|
||||
// Copyright 2016 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.
|
||||
|
||||
//go:build !go1.10
|
||||
|
||||
// Package idna implements IDNA2008 using the compatibility processing
|
||||
// defined by UTS (Unicode Technical Standard) #46, which defines a standard to
|
||||
// deal with the transition from IDNA2003.
|
||||
//
|
||||
// IDNA2008 (Internationalized Domain Names for Applications), is defined in RFC
|
||||
// 5890, RFC 5891, RFC 5892, RFC 5893 and RFC 5894.
|
||||
// UTS #46 is defined in https://www.unicode.org/reports/tr46.
|
||||
// See https://unicode.org/cldr/utility/idna.jsp for a visualization of the
|
||||
// differences between these two standards.
|
||||
package idna // import "golang.org/x/net/idna"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/secure/bidirule"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// NOTE: Unlike common practice in Go APIs, the functions will return a
|
||||
// sanitized domain name in case of errors. Browsers sometimes use a partially
|
||||
// evaluated string as lookup.
|
||||
// TODO: the current error handling is, in my opinion, the least opinionated.
|
||||
// Other strategies are also viable, though:
|
||||
// Option 1) Return an empty string in case of error, but allow the user to
|
||||
// specify explicitly which errors to ignore.
|
||||
// Option 2) Return the partially evaluated string if it is itself a valid
|
||||
// string, otherwise return the empty string in case of error.
|
||||
// Option 3) Option 1 and 2.
|
||||
// Option 4) Always return an empty string for now and implement Option 1 as
|
||||
// needed, and document that the return string may not be empty in case of
|
||||
// error in the future.
|
||||
// I think Option 1 is best, but it is quite opinionated.
|
||||
|
||||
// ToASCII is a wrapper for Punycode.ToASCII.
|
||||
func ToASCII(s string) (string, error) {
|
||||
return Punycode.process(s, true)
|
||||
}
|
||||
|
||||
// ToUnicode is a wrapper for Punycode.ToUnicode.
|
||||
func ToUnicode(s string) (string, error) {
|
||||
return Punycode.process(s, false)
|
||||
}
|
||||
|
||||
// An Option configures a Profile at creation time.
|
||||
type Option func(*options)
|
||||
|
||||
// Transitional sets a Profile to use the Transitional mapping as defined in UTS
|
||||
// #46. This will cause, for example, "ß" to be mapped to "ss". Using the
|
||||
// transitional mapping provides a compromise between IDNA2003 and IDNA2008
|
||||
// compatibility. It is used by some browsers when resolving domain names. This
|
||||
// option is only meaningful if combined with MapForLookup.
|
||||
func Transitional(transitional bool) Option {
|
||||
return func(o *options) { o.transitional = transitional }
|
||||
}
|
||||
|
||||
// VerifyDNSLength sets whether a Profile should fail if any of the IDN parts
|
||||
// are longer than allowed by the RFC.
|
||||
//
|
||||
// This option corresponds to the VerifyDnsLength flag in UTS #46.
|
||||
func VerifyDNSLength(verify bool) Option {
|
||||
return func(o *options) { o.verifyDNSLength = verify }
|
||||
}
|
||||
|
||||
// RemoveLeadingDots removes leading label separators. Leading runes that map to
|
||||
// dots, such as U+3002 IDEOGRAPHIC FULL STOP, are removed as well.
|
||||
func RemoveLeadingDots(remove bool) Option {
|
||||
return func(o *options) { o.removeLeadingDots = remove }
|
||||
}
|
||||
|
||||
// ValidateLabels sets whether to check the mandatory label validation criteria
|
||||
// as defined in Section 5.4 of RFC 5891. This includes testing for correct use
|
||||
// of hyphens ('-'), normalization, validity of runes, and the context rules.
|
||||
// In particular, ValidateLabels also sets the CheckHyphens and CheckJoiners flags
|
||||
// in UTS #46.
|
||||
func ValidateLabels(enable bool) Option {
|
||||
return func(o *options) {
|
||||
// Don't override existing mappings, but set one that at least checks
|
||||
// normalization if it is not set.
|
||||
if o.mapping == nil && enable {
|
||||
o.mapping = normalize
|
||||
}
|
||||
o.trie = trie
|
||||
o.checkJoiners = enable
|
||||
o.checkHyphens = enable
|
||||
if enable {
|
||||
o.fromPuny = validateFromPunycode
|
||||
} else {
|
||||
o.fromPuny = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckHyphens sets whether to check for correct use of hyphens ('-') in
|
||||
// labels. Most web browsers do not have this option set, since labels such as
|
||||
// "r3---sn-apo3qvuoxuxbt-j5pe" are in common use.
|
||||
//
|
||||
// This option corresponds to the CheckHyphens flag in UTS #46.
|
||||
func CheckHyphens(enable bool) Option {
|
||||
return func(o *options) { o.checkHyphens = enable }
|
||||
}
|
||||
|
||||
// CheckJoiners sets whether to check the ContextJ rules as defined in Appendix
|
||||
// A of RFC 5892, concerning the use of joiner runes.
|
||||
//
|
||||
// This option corresponds to the CheckJoiners flag in UTS #46.
|
||||
func CheckJoiners(enable bool) Option {
|
||||
return func(o *options) {
|
||||
o.trie = trie
|
||||
o.checkJoiners = enable
|
||||
}
|
||||
}
|
||||
|
||||
// StrictDomainName limits the set of permissible ASCII characters to those
|
||||
// allowed in domain names as defined in RFC 1034 (A-Z, a-z, 0-9 and the
|
||||
// hyphen). This is set by default for MapForLookup and ValidateForRegistration,
|
||||
// but is only useful if ValidateLabels is set.
|
||||
//
|
||||
// This option is useful, for instance, for browsers that allow characters
|
||||
// outside this range, for example a '_' (U+005F LOW LINE). See
|
||||
// http://www.rfc-editor.org/std/std3.txt for more details.
|
||||
//
|
||||
// This option corresponds to the UseSTD3ASCIIRules flag in UTS #46.
|
||||
func StrictDomainName(use bool) Option {
|
||||
return func(o *options) { o.useSTD3Rules = use }
|
||||
}
|
||||
|
||||
// NOTE: the following options pull in tables. The tables should not be linked
|
||||
// in as long as the options are not used.
|
||||
|
||||
// BidiRule enables the Bidi rule as defined in RFC 5893. Any application
|
||||
// that relies on proper validation of labels should include this rule.
|
||||
//
|
||||
// This option corresponds to the CheckBidi flag in UTS #46.
|
||||
func BidiRule() Option {
|
||||
return func(o *options) { o.bidirule = bidirule.ValidString }
|
||||
}
|
||||
|
||||
// ValidateForRegistration sets validation options to verify that a given IDN is
|
||||
// properly formatted for registration as defined by Section 4 of RFC 5891.
|
||||
func ValidateForRegistration() Option {
|
||||
return func(o *options) {
|
||||
o.mapping = validateRegistration
|
||||
StrictDomainName(true)(o)
|
||||
ValidateLabels(true)(o)
|
||||
VerifyDNSLength(true)(o)
|
||||
BidiRule()(o)
|
||||
}
|
||||
}
|
||||
|
||||
// MapForLookup sets validation and mapping options such that a given IDN is
|
||||
// transformed for domain name lookup according to the requirements set out in
|
||||
// Section 5 of RFC 5891. The mappings follow the recommendations of RFC 5894,
|
||||
// RFC 5895 and UTS 46. It does not add the Bidi Rule. Use the BidiRule option
|
||||
// to add this check.
|
||||
//
|
||||
// The mappings include normalization and mapping case, width and other
|
||||
// compatibility mappings.
|
||||
func MapForLookup() Option {
|
||||
return func(o *options) {
|
||||
o.mapping = validateAndMap
|
||||
StrictDomainName(true)(o)
|
||||
ValidateLabels(true)(o)
|
||||
RemoveLeadingDots(true)(o)
|
||||
}
|
||||
}
|
||||
|
||||
type options struct {
|
||||
transitional bool
|
||||
useSTD3Rules bool
|
||||
checkHyphens bool
|
||||
checkJoiners bool
|
||||
verifyDNSLength bool
|
||||
removeLeadingDots bool
|
||||
|
||||
trie *idnaTrie
|
||||
|
||||
// fromPuny calls validation rules when converting A-labels to U-labels.
|
||||
fromPuny func(p *Profile, s string) error
|
||||
|
||||
// mapping implements a validation and mapping step as defined in RFC 5895
|
||||
// or UTS 46, tailored to, for example, domain registration or lookup.
|
||||
mapping func(p *Profile, s string) (string, error)
|
||||
|
||||
// bidirule, if specified, checks whether s conforms to the Bidi Rule
|
||||
// defined in RFC 5893.
|
||||
bidirule func(s string) bool
|
||||
}
|
||||
|
||||
// A Profile defines the configuration of a IDNA mapper.
|
||||
type Profile struct {
|
||||
options
|
||||
}
|
||||
|
||||
func apply(o *options, opts []Option) {
|
||||
for _, f := range opts {
|
||||
f(o)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new Profile.
|
||||
//
|
||||
// With no options, the returned Profile is the most permissive and equals the
|
||||
// Punycode Profile. Options can be passed to further restrict the Profile. The
|
||||
// MapForLookup and ValidateForRegistration options set a collection of options,
|
||||
// for lookup and registration purposes respectively, which can be tailored by
|
||||
// adding more fine-grained options, where later options override earlier
|
||||
// options.
|
||||
func New(o ...Option) *Profile {
|
||||
p := &Profile{}
|
||||
apply(&p.options, o)
|
||||
return p
|
||||
}
|
||||
|
||||
// ToASCII converts a domain or domain label to its ASCII form. For example,
|
||||
// ToASCII("bücher.example.com") is "xn--bcher-kva.example.com", and
|
||||
// ToASCII("golang") is "golang". If an error is encountered it will return
|
||||
// an error and a (partially) processed result.
|
||||
func (p *Profile) ToASCII(s string) (string, error) {
|
||||
return p.process(s, true)
|
||||
}
|
||||
|
||||
// ToUnicode converts a domain or domain label to its Unicode form. For example,
|
||||
// ToUnicode("xn--bcher-kva.example.com") is "bücher.example.com", and
|
||||
// ToUnicode("golang") is "golang". If an error is encountered it will return
|
||||
// an error and a (partially) processed result.
|
||||
func (p *Profile) ToUnicode(s string) (string, error) {
|
||||
pp := *p
|
||||
pp.transitional = false
|
||||
return pp.process(s, false)
|
||||
}
|
||||
|
||||
// String reports a string with a description of the profile for debugging
|
||||
// purposes. The string format may change with different versions.
|
||||
func (p *Profile) String() string {
|
||||
s := ""
|
||||
if p.transitional {
|
||||
s = "Transitional"
|
||||
} else {
|
||||
s = "NonTransitional"
|
||||
}
|
||||
if p.useSTD3Rules {
|
||||
s += ":UseSTD3Rules"
|
||||
}
|
||||
if p.checkHyphens {
|
||||
s += ":CheckHyphens"
|
||||
}
|
||||
if p.checkJoiners {
|
||||
s += ":CheckJoiners"
|
||||
}
|
||||
if p.verifyDNSLength {
|
||||
s += ":VerifyDNSLength"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var (
|
||||
// Punycode is a Profile that does raw punycode processing with a minimum
|
||||
// of validation.
|
||||
Punycode *Profile = punycode
|
||||
|
||||
// Lookup is the recommended profile for looking up domain names, according
|
||||
// to Section 5 of RFC 5891. The exact configuration of this profile may
|
||||
// change over time.
|
||||
Lookup *Profile = lookup
|
||||
|
||||
// Display is the recommended profile for displaying domain names.
|
||||
// The configuration of this profile may change over time.
|
||||
Display *Profile = display
|
||||
|
||||
// Registration is the recommended profile for checking whether a given
|
||||
// IDN is valid for registration, according to Section 4 of RFC 5891.
|
||||
Registration *Profile = registration
|
||||
|
||||
punycode = &Profile{}
|
||||
lookup = &Profile{options{
|
||||
transitional: true,
|
||||
removeLeadingDots: true,
|
||||
useSTD3Rules: true,
|
||||
checkHyphens: true,
|
||||
checkJoiners: true,
|
||||
trie: trie,
|
||||
fromPuny: validateFromPunycode,
|
||||
mapping: validateAndMap,
|
||||
bidirule: bidirule.ValidString,
|
||||
}}
|
||||
display = &Profile{options{
|
||||
useSTD3Rules: true,
|
||||
removeLeadingDots: true,
|
||||
checkHyphens: true,
|
||||
checkJoiners: true,
|
||||
trie: trie,
|
||||
fromPuny: validateFromPunycode,
|
||||
mapping: validateAndMap,
|
||||
bidirule: bidirule.ValidString,
|
||||
}}
|
||||
registration = &Profile{options{
|
||||
useSTD3Rules: true,
|
||||
verifyDNSLength: true,
|
||||
checkHyphens: true,
|
||||
checkJoiners: true,
|
||||
trie: trie,
|
||||
fromPuny: validateFromPunycode,
|
||||
mapping: validateRegistration,
|
||||
bidirule: bidirule.ValidString,
|
||||
}}
|
||||
|
||||
// TODO: profiles
|
||||
// Register: recommended for approving domain names: don't do any mappings
|
||||
// but rather reject on invalid input. Bundle or block deviation characters.
|
||||
)
|
||||
|
||||
type labelError struct{ label, code_ string }
|
||||
|
||||
func (e labelError) code() string { return e.code_ }
|
||||
func (e labelError) Error() string {
|
||||
return fmt.Sprintf("idna: invalid label %q", e.label)
|
||||
}
|
||||
|
||||
type runeError rune
|
||||
|
||||
func (e runeError) code() string { return "P1" }
|
||||
func (e runeError) Error() string {
|
||||
return fmt.Sprintf("idna: disallowed rune %U", e)
|
||||
}
|
||||
|
||||
// process implements the algorithm described in section 4 of UTS #46,
|
||||
// see https://www.unicode.org/reports/tr46.
|
||||
func (p *Profile) process(s string, toASCII bool) (string, error) {
|
||||
var err error
|
||||
if p.mapping != nil {
|
||||
s, err = p.mapping(p, s)
|
||||
}
|
||||
// Remove leading empty labels.
|
||||
if p.removeLeadingDots {
|
||||
for ; len(s) > 0 && s[0] == '.'; s = s[1:] {
|
||||
}
|
||||
}
|
||||
// It seems like we should only create this error on ToASCII, but the
|
||||
// UTS 46 conformance tests suggests we should always check this.
|
||||
if err == nil && p.verifyDNSLength && s == "" {
|
||||
err = &labelError{s, "A4"}
|
||||
}
|
||||
labels := labelIter{orig: s}
|
||||
for ; !labels.done(); labels.next() {
|
||||
label := labels.label()
|
||||
if label == "" {
|
||||
// Empty labels are not okay. The label iterator skips the last
|
||||
// label if it is empty.
|
||||
if err == nil && p.verifyDNSLength {
|
||||
err = &labelError{s, "A4"}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(label, acePrefix) {
|
||||
u, err2 := decode(label[len(acePrefix):])
|
||||
if err2 != nil {
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
// Spec says keep the old label.
|
||||
continue
|
||||
}
|
||||
labels.set(u)
|
||||
if err == nil && p.fromPuny != nil {
|
||||
err = p.fromPuny(p, u)
|
||||
}
|
||||
if err == nil {
|
||||
// This should be called on NonTransitional, according to the
|
||||
// spec, but that currently does not have any effect. Use the
|
||||
// original profile to preserve options.
|
||||
err = p.validateLabel(u)
|
||||
}
|
||||
} else if err == nil {
|
||||
err = p.validateLabel(label)
|
||||
}
|
||||
}
|
||||
if toASCII {
|
||||
for labels.reset(); !labels.done(); labels.next() {
|
||||
label := labels.label()
|
||||
if !ascii(label) {
|
||||
a, err2 := encode(acePrefix, label)
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
label = a
|
||||
labels.set(a)
|
||||
}
|
||||
n := len(label)
|
||||
if p.verifyDNSLength && err == nil && (n == 0 || n > 63) {
|
||||
err = &labelError{label, "A4"}
|
||||
}
|
||||
}
|
||||
}
|
||||
s = labels.result()
|
||||
if toASCII && p.verifyDNSLength && err == nil {
|
||||
// Compute the length of the domain name minus the root label and its dot.
|
||||
n := len(s)
|
||||
if n > 0 && s[n-1] == '.' {
|
||||
n--
|
||||
}
|
||||
if len(s) < 1 || n > 253 {
|
||||
err = &labelError{s, "A4"}
|
||||
}
|
||||
}
|
||||
return s, err
|
||||
}
|
||||
|
||||
func normalize(p *Profile, s string) (string, error) {
|
||||
return norm.NFC.String(s), nil
|
||||
}
|
||||
|
||||
func validateRegistration(p *Profile, s string) (string, error) {
|
||||
if !norm.NFC.IsNormalString(s) {
|
||||
return s, &labelError{s, "V1"}
|
||||
}
|
||||
for i := 0; i < len(s); {
|
||||
v, sz := trie.lookupString(s[i:])
|
||||
// Copy bytes not copied so far.
|
||||
switch p.simplify(info(v).category()) {
|
||||
// TODO: handle the NV8 defined in the Unicode idna data set to allow
|
||||
// for strict conformance to IDNA2008.
|
||||
case valid, deviation:
|
||||
case disallowed, mapped, unknown, ignored:
|
||||
r, _ := utf8.DecodeRuneInString(s[i:])
|
||||
return s, runeError(r)
|
||||
}
|
||||
i += sz
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func validateAndMap(p *Profile, s string) (string, error) {
|
||||
var (
|
||||
err error
|
||||
b []byte
|
||||
k int
|
||||
)
|
||||
for i := 0; i < len(s); {
|
||||
v, sz := trie.lookupString(s[i:])
|
||||
start := i
|
||||
i += sz
|
||||
// Copy bytes not copied so far.
|
||||
switch p.simplify(info(v).category()) {
|
||||
case valid:
|
||||
continue
|
||||
case disallowed:
|
||||
if err == nil {
|
||||
r, _ := utf8.DecodeRuneInString(s[start:])
|
||||
err = runeError(r)
|
||||
}
|
||||
continue
|
||||
case mapped, deviation:
|
||||
b = append(b, s[k:start]...)
|
||||
b = info(v).appendMapping(b, s[start:i])
|
||||
case ignored:
|
||||
b = append(b, s[k:start]...)
|
||||
// drop the rune
|
||||
case unknown:
|
||||
b = append(b, s[k:start]...)
|
||||
b = append(b, "\ufffd"...)
|
||||
}
|
||||
k = i
|
||||
}
|
||||
if k == 0 {
|
||||
// No changes so far.
|
||||
s = norm.NFC.String(s)
|
||||
} else {
|
||||
b = append(b, s[k:]...)
|
||||
if norm.NFC.QuickSpan(b) != len(b) {
|
||||
b = norm.NFC.Bytes(b)
|
||||
}
|
||||
// TODO: the punycode converters require strings as input.
|
||||
s = string(b)
|
||||
}
|
||||
return s, err
|
||||
}
|
||||
|
||||
// A labelIter allows iterating over domain name labels.
|
||||
type labelIter struct {
|
||||
orig string
|
||||
slice []string
|
||||
curStart int
|
||||
curEnd int
|
||||
i int
|
||||
}
|
||||
|
||||
func (l *labelIter) reset() {
|
||||
l.curStart = 0
|
||||
l.curEnd = 0
|
||||
l.i = 0
|
||||
}
|
||||
|
||||
func (l *labelIter) done() bool {
|
||||
return l.curStart >= len(l.orig)
|
||||
}
|
||||
|
||||
func (l *labelIter) result() string {
|
||||
if l.slice != nil {
|
||||
return strings.Join(l.slice, ".")
|
||||
}
|
||||
return l.orig
|
||||
}
|
||||
|
||||
func (l *labelIter) label() string {
|
||||
if l.slice != nil {
|
||||
return l.slice[l.i]
|
||||
}
|
||||
p := strings.IndexByte(l.orig[l.curStart:], '.')
|
||||
l.curEnd = l.curStart + p
|
||||
if p == -1 {
|
||||
l.curEnd = len(l.orig)
|
||||
}
|
||||
return l.orig[l.curStart:l.curEnd]
|
||||
}
|
||||
|
||||
// next sets the value to the next label. It skips the last label if it is empty.
|
||||
func (l *labelIter) next() {
|
||||
l.i++
|
||||
if l.slice != nil {
|
||||
if l.i >= len(l.slice) || l.i == len(l.slice)-1 && l.slice[l.i] == "" {
|
||||
l.curStart = len(l.orig)
|
||||
}
|
||||
} else {
|
||||
l.curStart = l.curEnd + 1
|
||||
if l.curStart == len(l.orig)-1 && l.orig[l.curStart] == '.' {
|
||||
l.curStart = len(l.orig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *labelIter) set(s string) {
|
||||
if l.slice == nil {
|
||||
l.slice = strings.Split(l.orig, ".")
|
||||
}
|
||||
l.slice[l.i] = s
|
||||
}
|
||||
|
||||
// acePrefix is the ASCII Compatible Encoding prefix.
|
||||
const acePrefix = "xn--"
|
||||
|
||||
func (p *Profile) simplify(cat category) category {
|
||||
switch cat {
|
||||
case disallowedSTD3Mapped:
|
||||
if p.useSTD3Rules {
|
||||
cat = disallowed
|
||||
} else {
|
||||
cat = mapped
|
||||
}
|
||||
case disallowedSTD3Valid:
|
||||
if p.useSTD3Rules {
|
||||
cat = disallowed
|
||||
} else {
|
||||
cat = valid
|
||||
}
|
||||
case deviation:
|
||||
if !p.transitional {
|
||||
cat = valid
|
||||
}
|
||||
case validNV8, validXV8:
|
||||
// TODO: handle V2008
|
||||
cat = valid
|
||||
}
|
||||
return cat
|
||||
}
|
||||
|
||||
func validateFromPunycode(p *Profile, s string) error {
|
||||
if !norm.NFC.IsNormalString(s) {
|
||||
return &labelError{s, "V1"}
|
||||
}
|
||||
for i := 0; i < len(s); {
|
||||
v, sz := trie.lookupString(s[i:])
|
||||
if c := p.simplify(info(v).category()); c != valid && c != deviation {
|
||||
return &labelError{s, "V6"}
|
||||
}
|
||||
i += sz
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
zwnj = "\u200c"
|
||||
zwj = "\u200d"
|
||||
)
|
||||
|
||||
type joinState int8
|
||||
|
||||
const (
|
||||
stateStart joinState = iota
|
||||
stateVirama
|
||||
stateBefore
|
||||
stateBeforeVirama
|
||||
stateAfter
|
||||
stateFAIL
|
||||
)
|
||||
|
||||
var joinStates = [][numJoinTypes]joinState{
|
||||
stateStart: {
|
||||
joiningL: stateBefore,
|
||||
joiningD: stateBefore,
|
||||
joinZWNJ: stateFAIL,
|
||||
joinZWJ: stateFAIL,
|
||||
joinVirama: stateVirama,
|
||||
},
|
||||
stateVirama: {
|
||||
joiningL: stateBefore,
|
||||
joiningD: stateBefore,
|
||||
},
|
||||
stateBefore: {
|
||||
joiningL: stateBefore,
|
||||
joiningD: stateBefore,
|
||||
joiningT: stateBefore,
|
||||
joinZWNJ: stateAfter,
|
||||
joinZWJ: stateFAIL,
|
||||
joinVirama: stateBeforeVirama,
|
||||
},
|
||||
stateBeforeVirama: {
|
||||
joiningL: stateBefore,
|
||||
joiningD: stateBefore,
|
||||
joiningT: stateBefore,
|
||||
},
|
||||
stateAfter: {
|
||||
joiningL: stateFAIL,
|
||||
joiningD: stateBefore,
|
||||
joiningT: stateAfter,
|
||||
joiningR: stateStart,
|
||||
joinZWNJ: stateFAIL,
|
||||
joinZWJ: stateFAIL,
|
||||
joinVirama: stateAfter, // no-op as we can't accept joiners here
|
||||
},
|
||||
stateFAIL: {
|
||||
0: stateFAIL,
|
||||
joiningL: stateFAIL,
|
||||
joiningD: stateFAIL,
|
||||
joiningT: stateFAIL,
|
||||
joiningR: stateFAIL,
|
||||
joinZWNJ: stateFAIL,
|
||||
joinZWJ: stateFAIL,
|
||||
joinVirama: stateFAIL,
|
||||
},
|
||||
}
|
||||
|
||||
// validateLabel validates the criteria from Section 4.1. Item 1, 4, and 6 are
|
||||
// already implicitly satisfied by the overall implementation.
|
||||
func (p *Profile) validateLabel(s string) error {
|
||||
if s == "" {
|
||||
if p.verifyDNSLength {
|
||||
return &labelError{s, "A4"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if p.bidirule != nil && !p.bidirule(s) {
|
||||
return &labelError{s, "B"}
|
||||
}
|
||||
if p.checkHyphens {
|
||||
if len(s) > 4 && s[2] == '-' && s[3] == '-' {
|
||||
return &labelError{s, "V2"}
|
||||
}
|
||||
if s[0] == '-' || s[len(s)-1] == '-' {
|
||||
return &labelError{s, "V3"}
|
||||
}
|
||||
}
|
||||
if !p.checkJoiners {
|
||||
return nil
|
||||
}
|
||||
trie := p.trie // p.checkJoiners is only set if trie is set.
|
||||
// TODO: merge the use of this in the trie.
|
||||
v, sz := trie.lookupString(s)
|
||||
x := info(v)
|
||||
if x.isModifier() {
|
||||
return &labelError{s, "V5"}
|
||||
}
|
||||
// Quickly return in the absence of zero-width (non) joiners.
|
||||
if strings.Index(s, zwj) == -1 && strings.Index(s, zwnj) == -1 {
|
||||
return nil
|
||||
}
|
||||
st := stateStart
|
||||
for i := 0; ; {
|
||||
jt := x.joinType()
|
||||
if s[i:i+sz] == zwj {
|
||||
jt = joinZWJ
|
||||
} else if s[i:i+sz] == zwnj {
|
||||
jt = joinZWNJ
|
||||
}
|
||||
st = joinStates[st][jt]
|
||||
if x.isViramaModifier() {
|
||||
st = joinStates[st][joinVirama]
|
||||
}
|
||||
if i += sz; i == len(s) {
|
||||
break
|
||||
}
|
||||
v, sz = trie.lookupString(s[i:])
|
||||
x = info(v)
|
||||
}
|
||||
if st == stateFAIL || st == stateAfter {
|
||||
return &labelError{s, "C"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ascii(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] >= utf8.RuneSelf {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
|
||||
|
||||
// Copyright 2021 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.
|
||||
|
||||
//go:build !go1.18
|
||||
|
||||
package idna
|
||||
|
||||
const transitionalLookup = true
|
||||
+4
-1
@@ -28,7 +28,7 @@ const (
|
||||
tmin int32 = 1
|
||||
)
|
||||
|
||||
func punyError(s string) error { return &labelError{s, "A3"} }
|
||||
func punyError(s string) error { return &labelError{s, code16("A3", "P4")} }
|
||||
|
||||
// decode decodes a string as specified in section 6.2.
|
||||
func decode(encoded string) (string, error) {
|
||||
@@ -108,6 +108,9 @@ func encode(prefix, s string) (string, error) {
|
||||
delta, n, bias := int32(0), initialN, initialBias
|
||||
b, remaining := int32(0), int32(0)
|
||||
for _, r := range s {
|
||||
if unicode16 && r == 0xfffd {
|
||||
return s, &labelError{s, "A3"}
|
||||
}
|
||||
if r < 0x80 {
|
||||
b++
|
||||
output = append(output, byte(r))
|
||||
|
||||
-4559
File diff suppressed because it is too large
Load Diff
-4653
File diff suppressed because it is too large
Load Diff
-4733
File diff suppressed because it is too large
Load Diff
-4959
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
||||
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
|
||||
|
||||
//go:build go1.21
|
||||
//go:build !go1.27
|
||||
|
||||
package idna
|
||||
|
||||
|
||||
+5302
File diff suppressed because it is too large
Load Diff
-4486
File diff suppressed because it is too large
Load Diff
-30
@@ -1,30 +0,0 @@
|
||||
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
|
||||
|
||||
// Copyright 2016 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.
|
||||
|
||||
//go:build !go1.16
|
||||
|
||||
package idna
|
||||
|
||||
// appendMapping appends the mapping for the respective rune. isMapped must be
|
||||
// true. A mapping is a categorization of a rune as defined in UTS #46.
|
||||
func (c info) appendMapping(b []byte, s string) []byte {
|
||||
index := int(c >> indexShift)
|
||||
if c&xorBit == 0 {
|
||||
s := mappings[index:]
|
||||
return append(b, s[1:s[0]+1]...)
|
||||
}
|
||||
b = append(b, s...)
|
||||
if c&inlineXOR == inlineXOR {
|
||||
// TODO: support and handle two-byte inline masks
|
||||
b[len(b)-1] ^= byte(index)
|
||||
} else {
|
||||
for p := len(b) - int(xorData[index]); p < len(b); p++ {
|
||||
index++
|
||||
b[p] ^= xorData[index]
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
|
||||
|
||||
// Copyright 2016 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.
|
||||
|
||||
//go:build go1.16
|
||||
|
||||
package idna
|
||||
|
||||
// appendMapping appends the mapping for the respective rune. isMapped must be
|
||||
// true. A mapping is a categorization of a rune as defined in UTS #46.
|
||||
func (c info) appendMapping(b []byte, s string) []byte {
|
||||
index := int(c >> indexShift)
|
||||
if c&xorBit == 0 {
|
||||
p := index
|
||||
return append(b, mappings[mappingIndex[p]:mappingIndex[p+1]]...)
|
||||
}
|
||||
b = append(b, s...)
|
||||
if c&inlineXOR == inlineXOR {
|
||||
// TODO: support and handle two-byte inline masks
|
||||
b[len(b)-1] ^= byte(index)
|
||||
} else {
|
||||
for p := len(b) - int(xorData[index]); p < len(b); p++ {
|
||||
index++
|
||||
b[p] ^= xorData[index]
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
+8
@@ -448,6 +448,14 @@ func NewServerRequest(rp ServerRequestParam) ServerRequestResult {
|
||||
url_ = &url.URL{Host: rp.Authority}
|
||||
requestURI = rp.Authority // mimic HTTP/1 server behavior
|
||||
} else {
|
||||
// "[The :path] pseudo-header field MUST NOT be empty [...]"
|
||||
// https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.1-2.4.2
|
||||
if rp.Path == "" || (rp.Path[0] != '/' && rp.Path != "*") {
|
||||
return ServerRequestResult{
|
||||
InvalidReason: "bad_path",
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
url_, err = url.ParseRequestURI(rp.Path)
|
||||
if err != nil {
|
||||
|
||||
+15
-6
@@ -182,12 +182,13 @@ var MIPS64X struct {
|
||||
// require kernel support to work (DARN, SCV), so there are feature bits for
|
||||
// those as well. The struct is padded to avoid false sharing.
|
||||
var PPC64 struct {
|
||||
_ CacheLinePad
|
||||
HasDARN bool // Hardware random number generator (requires kernel enablement)
|
||||
HasSCV bool // Syscall vectored (requires kernel enablement)
|
||||
IsPOWER8 bool // ISA v2.07 (POWER8)
|
||||
IsPOWER9 bool // ISA v3.00 (POWER9), implies IsPOWER8
|
||||
_ CacheLinePad
|
||||
_ CacheLinePad
|
||||
HasDARN bool // Hardware random number generator (requires kernel enablement)
|
||||
HasSCV bool // Syscall vectored (requires kernel enablement)
|
||||
IsPOWER8 bool // ISA v2.07 (POWER8)
|
||||
IsPOWER9 bool // ISA v3.00 (POWER9), implies IsPOWER8
|
||||
IsPOWER10 bool // ISA v3.1 (POWER10 and POWER11; POWER11 did not add a new architected level), implies IsPOWER9
|
||||
_ CacheLinePad
|
||||
}
|
||||
|
||||
// S390X contains the supported CPU features of the current IBM Z
|
||||
@@ -248,13 +249,21 @@ var RISCV64 struct {
|
||||
HasZvks bool // ShangMi Algorithm Suite
|
||||
HasZvksc bool // ShangMi Algorithm Suite with carryless multiplication
|
||||
HasZvksg bool // ShangMi Algorithm Suite with GCM
|
||||
VLENB uint // Vector register length in bytes, 0 if undetected
|
||||
_ CacheLinePad
|
||||
}
|
||||
|
||||
// doDerived, if non-nil, is called after processing GODEBUG to set "derived"
|
||||
// feature flags.
|
||||
var doDerived func()
|
||||
|
||||
func init() {
|
||||
archInit()
|
||||
initOptions()
|
||||
processOptions()
|
||||
if doDerived != nil {
|
||||
doDerived()
|
||||
}
|
||||
}
|
||||
|
||||
// options contains the cpu debug options that can be used in GODEBUG.
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// Copyright 2026 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build gc
|
||||
|
||||
package cpu
|
||||
|
||||
// Can only be called when the vector extension is present.
|
||||
// Implemented in cpu_riscv64.s.
|
||||
func readVLENB() uint
|
||||
+4
@@ -11,6 +11,7 @@ const (
|
||||
// ISA Level
|
||||
_PPC_FEATURE2_ARCH_2_07 = 0x80000000
|
||||
_PPC_FEATURE2_ARCH_3_00 = 0x00800000
|
||||
_PPC_FEATURE2_ARCH_3_1 = 0x00040000
|
||||
|
||||
// CPU features
|
||||
_PPC_FEATURE2_DARN = 0x00200000
|
||||
@@ -21,6 +22,9 @@ func doinit() {
|
||||
// HWCAP2 feature bits
|
||||
PPC64.IsPOWER8 = isSet(hwCap2, _PPC_FEATURE2_ARCH_2_07)
|
||||
PPC64.IsPOWER9 = isSet(hwCap2, _PPC_FEATURE2_ARCH_3_00)
|
||||
// ISA 3.1 covers both POWER10 and POWER11: POWER11 did not introduce a
|
||||
// new architected HWCAP level, so there is no separate IsPOWER11.
|
||||
PPC64.IsPOWER10 = isSet(hwCap2, _PPC_FEATURE2_ARCH_3_1)
|
||||
PPC64.HasDARN = isSet(hwCap2, _PPC_FEATURE2_DARN)
|
||||
PPC64.HasSCV = isSet(hwCap2, _PPC_FEATURE2_SCV)
|
||||
}
|
||||
|
||||
+11
@@ -130,6 +130,9 @@ func doinit() {
|
||||
RISCV64.HasFastMisaligned = v == riscv_HWPROBE_MISALIGNED_FAST
|
||||
}
|
||||
}
|
||||
if RISCV64.HasV {
|
||||
RISCV64.VLENB = readVLENB()
|
||||
}
|
||||
|
||||
// Let's double check with HWCAP if the C extension does not appear to be supported.
|
||||
// This may happen if we're running on a kernel older than 6.4.
|
||||
@@ -137,6 +140,14 @@ func doinit() {
|
||||
if !RISCV64.HasC {
|
||||
RISCV64.HasC = isSet(hwCap, hwcap_RISCV_ISA_C)
|
||||
}
|
||||
|
||||
doDerived = func() {
|
||||
// If the vector extension is disabled by GODEBUG, then the VLENB is zero.
|
||||
if !RISCV64.HasV {
|
||||
RISCV64.VLENB = 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func isSet(hwc uint, value uint) bool {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user