Compare commits

..
Author SHA1 Message Date
renovate-bot 0dd314b53c chore(deps): update nginx docker tag to v1.31.4
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-08-19 21:01:10 +00:00
renovate-bot e9f51526f6 chore(deps): update golang docker tag
continuous-integration/drone/pr Build encountered an error
continuous-integration/drone/push Build encountered an error
2026-08-19 20:01:03 +00:00
renovate-bot ae2080fd9d chore(deps): update golang docker tag
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-08-19 15:01:28 +00:00
renovate-bot 8f6ebac2ce chore(deps): update module github.com/stretchr/testify to v1.12.1
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-08-19 14:01:40 +00:00
renovate-bot 23d67677c6 chore(deps): update golang docker tag
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-08-17 10:01:49 +00:00
renovate-bot 00d64556bc chore(deps): update module github.com/stretchr/testify to v1.12.0
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2026-08-17 09:04:16 +00:00
ChasquiLabo 70c0accf7f chore: translation using Weblate (Spanish)
continuous-integration/drone/push Build is passing
Currently translated at 90.6% (1046 of 1154 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-08-11 06:51:47 +00:00
ChasquiLabo 82ae4cb10f chore: translation using Weblate (Spanish)
continuous-integration/drone/push Build is passing
Currently translated at 90.6% (1046 of 1154 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-08-10 22:00:01 +00:00
ChasquiLabo 99d3cc87b2 chore: translation using Weblate (Spanish)
continuous-integration/drone/push Build is passing
Currently translated at 90.6% (1046 of 1154 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-08-10 02:00:46 +00:00
ChasquiLabo 232d6f39e4 chore: translation using Weblate (Spanish)
continuous-integration/drone/push Build is passing
Currently translated at 90.6% (1046 of 1154 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-08-09 21:49:50 +00:00
renovate-bot 26089f0886 chore(deps): update golang docker tag
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-07-29 14:00:59 +00:00
renovate-bot a1de95a8ce chore(deps): update module github.com/go-git/go-git/v5 to v5.19.2
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-07-29 12:02:00 +00:00
renovate-bot f4861bf3c1 chore(deps): update otel/weaver docker tag to v0.25.1
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-07-29 02:00:47 +00:00
renovate-bot 7fb3b2f591 chore(deps): update otel/weaver docker tag to v0.25.0
continuous-integration/drone/push Build is passing
2026-07-24 20:01:06 +00:00
renovate-bot 0dec68294f chore(deps): update nginx docker tag to v1.31.3
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-07-16 02:01:04 +00:00
renovate-bot 69ba548dab chore(deps): update dependency codespell to v2.4.3
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-07-15 12:00:52 +00:00
renovate-bot cfa9ff15bb chore(deps): update golang docker tag
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-07-08 18:01:07 +00:00
renovate-bot ddf7441152 chore(deps): update module golang.org/x/term to v0.45.0
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-07-08 17:01:50 +00:00
renovate-bot 20b4a3fb84 chore(deps): update golang docker tag
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-07-08 14:01:06 +00:00
renovate-bot 81f4420a67 chore(deps): update module golang.org/x/sys to v0.47.0
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-07-08 13:01:44 +00:00
157 changed files with 8886 additions and 26799 deletions
+3 -3
View File
@@ -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:
+1 -1
View File
@@ -1,5 +1,5 @@
# Build image
FROM golang:1.26-alpine AS build
FROM golang:1.27-alpine AS build
ENV GOPRIVATE=coopcloud.tech
-42
View File
@@ -84,45 +84,3 @@ build-mo:
release:
@goreleaser release --clean
CLIENT_VM := abra-client
vm-client-status:
sudo systemctl status microvm@$(CLIENT_VM).service
vm-client-create:test-integration-hosts
sudo microvm -f git+file://$$(pwd) -c $(CLIENT_VM)
vm-client-start:
sudo systemctl start microvm@$(CLIENT_VM).service
vm-client-update:test-integration-hosts
sudo microvm -R -f git+file://$$(pwd) -u $(CLIENT_VM)
vm-client-run:
sudo microvm -f git+file://$$(pwd) -r $(CLIENT_VM)
vm-client-stop:
sudo systemctl stop microvm@$(CLIENT_VM)
vm-client-delete: vm-client-stop
sudo rm -rf /var/lib/microvms/$(CLIENT_VM)
vm-client-connect:
ssh abra@10.0.0.2 -i ./tests/resources/local_integration_ssh
SERVER_VM := abra-server
vm-server-status:
sudo systemctl status microvm@$(SERVER_VM).service
vm-server-create:
sudo microvm -f git+file://$$(pwd) -c $(SERVER_VM)
vm-server-start:
sudo systemctl start microvm@$(SERVER_VM).service
vm-server-update:
sudo microvm -R -f git+file://$$(pwd) -u $(SERVER_VM)
vm-server-run:
sudo microvm -f git+file://$$(pwd) -r $(SERVER_VM)
vm-server-stop:
sudo systemctl stop microvm@$(SERVER_VM)
vm-server-delete: vm-server-stop
sudo rm -rf /var/lib/microvms/$(SERVER_VM)
vm-server-connect:
ssh abra@10.0.0.3 -i ./tests/resources/local_integration_ssh
test-integration-hosts:
./scripts/tests/extra_hosts
test-integration:
ssh-add ./tests/resources/local_integration_ssh
@bats -Tp tests/integration --verbose-run
Generated
-38
View File
@@ -18,27 +18,6 @@
"type": "github"
}
},
"microvm": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"spectrum": "spectrum"
},
"locked": {
"lastModified": 1784666190,
"narHash": "sha256-xgfS6slV7J3baMooNN1UuBi51RIgg9y0DbCxfSA0668=",
"owner": "astro",
"repo": "microvm.nix",
"rev": "fa5340ac684cdce8a22b6d4a0bcebb0cc999275e",
"type": "github"
},
"original": {
"owner": "astro",
"repo": "microvm.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1778443072,
@@ -58,26 +37,9 @@
"root": {
"inputs": {
"flake-utils": "flake-utils",
"microvm": "microvm",
"nixpkgs": "nixpkgs"
}
},
"spectrum": {
"flake": false,
"locked": {
"lastModified": 1783694892,
"narHash": "sha256-xO8f7Qng+18FK2UlB9vcrkxCaQMCt5WjCH24aW/11eg=",
"ref": "refs/heads/main",
"rev": "24c4346e30fdea8d8e80f34aec3554a15a667d24",
"revCount": 1410,
"type": "git",
"url": "https://spectrum-os.org/git/spectrum"
},
"original": {
"type": "git",
"url": "https://spectrum-os.org/git/spectrum"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
+18 -87
View File
@@ -4,103 +4,34 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
microvm = {
url = "github:astro/microvm.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
self,
nixpkgs,
microvm,
flake-utils,
}:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
publicKey = builtins.readFile ./tests/resources/local_integration_ssh.pub;
privateKey = builtins.readFile ./tests/resources/local_integration_ssh;
pathToRepo = builtins.readFile ./tests/resources/path_to_repo;
# local DNS aliases for the server host
extraHosts = builtins.readFile ./tests/resources/extra_hosts;
username = "abra";
password = "abra";
defaultDNS = [
# Quad9.net
"9.9.9.9"
"149.112.112.112"
"2620:fe::fe"
"2620:fe::9"
];
in
{
nixosConfigurations = {
abra-client = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
microvm.nixosModules.microvm
./nix/hosts/client/configuration.nix
];
specialArgs = {
inherit
publicKey
privateKey
username
password
defaultDNS
extraHosts
pathToRepo
;
};
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages = rec {
abra = pkgs.callPackage ./package.nix { };
default = abra;
};
abra-server = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
microvm.nixosModules.microvm
./nix/hosts/server/configuration.nix
];
specialArgs = {
inherit
publicKey
privateKey
username
password
defaultDNS
;
};
apps = rec {
abra = flake-utils.lib.mkApp { drv = self.packages.${system}.abra; };
default = abra;
};
};
nixosModules = rec {
host = {
imports = [
microvm.nixosModules.host
./nix/modules/host.nix
devShells.default = pkgs.mkShell {
packages = with pkgs; [
go_1_26
gnumake
];
};
default = host;
};
packages = rec {
abra = pkgs.callPackage ./nix/package.nix { };
default = abra;
};
apps = rec {
abra = flake-utils.lib.mkApp { drv = self.packages.${system}.abra; };
default = abra;
};
devShells.${system}.default = pkgs.mkShell {
# testing env variables
BATS_LIB_PATH = "~/.local/share/bats/";
TEST_SERVER = "abra.local";
ABRA_DIR = "$HOME/.abra_test";
packages = with pkgs; [
go_1_26
gnumake
gopls
];
};
};
}
);
}
+8 -10
View File
@@ -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.45.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.47.0
)
replace github.com/docker/cli v28.4.0+incompatible => git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible
+16 -17
View File
@@ -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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
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=
-72
View File
@@ -1,72 +0,0 @@
# Hosts
These hosts are VMs used for running the integration tests on your local machine.
The hosts contain a client where abra with the tests will be run and a server
to simulate a remote machine.
## Prepare the host
For this setup to work you need a machine with NixOS and flakes enabled.
1. Import the abra flake
The install example is based on the [using nix flakes wiki page](https://nixos.wiki/wiki/flakes#Using_nix_flakes_with_NixOS).
```nix
inputs = {
abra = {
url = "git+https://git.coopcloud.tech/toolshed/abra.git";
};
};
```
2. Add the host module to your configuration
Now add the host module to your configuration. At the toplevel of a flake it could look like this:
```nix
desktop = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
abra.nixosModules.host
./configuration.nix
];
specialArgs = inputs;
};
```
3. Adjust the required config
```nix
abra.testing = {
enable = true; # loads the required config, when set true
externalInterface = "eth1"; # adjust this to your network interface that has access to the internet
};
```
You can look up the interface with `ip link`. After that rebuild your system and you are ready to go.
## Get started
All commands are run on the repository root path.
1. Set path to repo
The VM will create a shared volume from the content of the file `tests/resources/path_to_repo`.
Set the content of the file to the path of your abra repository.
This will share the repository from your machine with the VM and you can edit files while testing without rebuiling
or restarting the VM.
2. Create the VMs
```sh
make vm-client-create
make vm-server-create
```
3. Start the VMs
```sh
make vm-client-start
make vm-server-start
```
After running that command you should be able to ping the machines.
The client runs on 10.0.0.2 and server 10.0.0.3
4. Connect to the client
Running the following command will connect to the client VM via SSH.
```sh
make vm-client-connect
```
5. Run the tests
```sh
make test-integration
```
-145
View File
@@ -1,145 +0,0 @@
{
lib,
pkgs,
publicKey,
username,
pathToRepo,
defaultDNS,
extraHosts,
...
}:
let
index = 2;
mac = "00:00:00:00:00:01";
serverIp = "10.0.0.3";
in
{
imports = [
../../modules/ssh.nix
../../modules/user.nix
../../modules/docker.nix
];
microvm = {
vcpu = 4;
mem = 2049; # see issue related in microvm repo with 2048, so add 1MB
interfaces = [
{
id = "vm${toString index}";
type = "tap";
inherit mac;
}
];
shares = [
{
proto = "virtiofs";
tag = "repo";
# Source path can be absolute or relative
# to /var/lib/microvms/$hostName
source = "${lib.trim pathToRepo}";
mountPoint = "/home/${username}/abra";
}
];
};
boot.tmp = {
useTmpfs = true;
tmpfsSize = "2G";
};
networking = {
hostName = "abra-client";
useNetworkd = true;
};
systemd.network.networks."10-eth" = {
matchConfig.MACAddress = mac;
# Static IP configuration
address = [
"10.0.0.${toString index}/32"
"fec0::${lib.toHexString index}/128"
];
routes = [
{
# A route to the host
Destination = "10.0.0.0/32";
GatewayOnLink = true;
}
{
# Route to server
Destination = "${serverIp}/32";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "0.0.0.0/0";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "::/0";
Gateway = "fec0::";
GatewayOnLink = true;
}
];
networkConfig = {
# DNS servers no longer come from DHCP nor Router
# Advertisements. Perhaps you want to change the defaults:
DNS = defaultDNS;
};
};
# open the ports to allow ssh access from the host
networking.firewall.allowedTCPPorts = [ 22 ];
networking.firewall.allowedUDPPorts = [ 22 ];
networking.extraHosts = extraHosts;
environment.variables = {
CGO_ENABLED = 0;
TEST_SERVER = "abra.local";
ABRA_DIR = "$HOME/.abra_test";
};
programs.ssh = {
startAgent = true;
knownHostsFiles = [
(pkgs.writeText "local.keys" ''
abra.local ${publicKey}
'')
];
extraConfig = ''
Host abra.local
HostName abra.local
Port 22
User ${username}
'';
};
programs.git = {
enable = true;
# many recipe commands need a preset git user
config = [
{
user = {
name = "abra dev";
email = "helo@coopcloud.tech";
};
}
];
};
environment.systemPackages = with pkgs; [
# build dependencies, copied from flake.nix
go_1_26
gnumake
# testing dependencies
(bats.withLibraries (p: [
p.bats-assert
p.bats-file
p.bats-support
]))
jq
wget
];
}
-42
View File
@@ -1,42 +0,0 @@
{
serverIp,
username,
...
}:
{
# Home Manager needs a bit of information about you and the
# paths it should manage.
home.username = username;
home.homeDirectory = "/home/${username}";
# This value determines the Home Manager release that your
# configuration is compatible with. This helps avoid breakage
# when a new Home Manager release introduces backwards
# incompatible changes.
#
# You can update Home Manager without changing this value. See
# the Home Manager release notes for a list of state version
# changes in each release.
home.stateVersion = "26.05";
# Let Home Manager install and manage itself.
programs.home-manager.enable = true;
programs.ssh = {
enable = true;
enableDefaultConfig = false;
settings = {
"abra.local" = {
HostName = serverIp;
User = username;
Port = 22;
IdentityFile = "/home/${username}/abra/tests/resources/local_integration_ssh";
};
"*.abra.local" = {
HostName = serverIp;
User = username;
Port = 22;
IdentityFile = "/home/${username}/abra/tests/resources/local_integration_sshn";
};
};
};
}
-91
View File
@@ -1,91 +0,0 @@
{
lib,
defaultDNS,
...
}:
let
index = 3;
mac = "00:00:00:00:00:02";
clientIp = "10.0.0.2";
in
{
imports = [
../../modules/ssh.nix
../../modules/user.nix
../../modules/docker.nix
];
microvm = {
vcpu = 4;
mem = 8097; # see issue related in microvm repo with 2048, so add 1MB
interfaces = [
{
id = "vm${toString index}";
type = "tap";
inherit mac;
}
];
};
boot.tmp = {
useTmpfs = true;
tmpfsSize = "8G";
};
networking = {
hostName = "abra-server";
useNetworkd = true;
};
systemd.network.networks."11-eth" = {
matchConfig.MACAddress = mac;
# Static IP configuration
address = [
"10.0.0.${toString index}/32"
"fec0::${lib.toHexString index}/128"
];
routes = [
{
# A route to the host
Destination = "10.0.0.0/32";
GatewayOnLink = true;
}
{
# Route to server
Destination = "${clientIp}/32";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "0.0.0.0/0";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "::/0";
Gateway = "fec0::";
GatewayOnLink = true;
}
];
networkConfig = {
# DNS servers no longer come from DHCP nor Router
# Advertisements. Perhaps you want to change the defaults:
DNS = defaultDNS;
};
};
# open the ports to allow ssh access from the host
networking.firewall.allowedTCPPorts = [
22
80
443
1312 # deploy with udp and tcp on same port test
];
networking.firewall.allowedUDPPorts = [
22
80
443
1312 # deploy with udp and tcp on same port test
];
}
-7
View File
@@ -1,7 +0,0 @@
{ ... }:
{
virtualisation.docker = {
enable = true;
liveRestore = false;
};
}
-59
View File
@@ -1,59 +0,0 @@
{
lib,
config,
...
}:
let
maxVMs = 3;
in
{
options = {
abra.testing.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable the abra integration testing framework";
};
abra.testing.externalInterface = lib.mkOption {
type = lib.types.str;
default = null;
description = "Change this to the interface with upstream Internet access";
};
};
config = lib.mkIf config.abra.testing.enable {
networking.useNetworkd = true;
systemd.network.wait-online.enable = false;
systemd.network.networks = builtins.listToAttrs (
map (index: {
name = "30-vm${toString index}";
value = {
matchConfig.Name = "vm${toString index}";
# Host's addresses
address = [
"10.0.0.0/32"
"fec0::/128"
];
# Setup routes to the VM
routes = [
{
Destination = "10.0.0.${toString index}/32";
}
{
Destination = "fec0::${lib.toHexString index}/128";
}
];
# Enable routing
networkConfig = {
IPv4Forwarding = true;
IPv6Forwarding = true;
};
};
}) (lib.genList (i: i + 1) maxVMs)
);
networking.nat = {
enable = true;
internalIPs = [ "10.0.0.0/24" ];
externalInterface = config.abra.testing.externalInterface;
};
};
}
-33
View File
@@ -1,33 +0,0 @@
{
privateKey,
publicKey,
username,
...
}:
{
# default host key location
environment.etc."ssh/ssh_host_ed25519_key" = {
text = privateKey;
mode = "0600";
};
environment.etc."ssh/ssh_host_ed25519_key.pub" = {
text = publicKey;
mode = "0644";
};
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
AllowUsers = [ "${username}" ];
};
generateHostKeys = false;
hostKeys = [
{
path = "/etc/ssh/ssh_host_ed25519_key";
type = "ed25519";
}
];
};
}
-22
View File
@@ -1,22 +0,0 @@
{
username,
password,
publicKey,
...
}:
{
users.users."${username}" = {
isNormalUser = true;
password = password;
home = "/home/${username}";
description = "Abra user";
extraGroups = [
"wheel"
"networkmanager"
"docker"
];
openssh.authorizedKeys.keys = [
publicKey
];
};
}
View File
Binary file not shown.
+5 -57
View File
@@ -1,17 +1,5 @@
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"
"Last-Translator: chasqui <chasqui@cryptolab.net>\n"
"Language-Team: Spanish <https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.12.2\n"
msgstr "Project-Id-Version: \nReport-Msgid-Bugs-To: EMAIL\nPOT-Creation-Date: 2026-06-14 17:56+0200\nPO-Revision-Date: 2026-08-11 06:51+0000\nLast-Translator: chasqui <chasqui@cryptolab.net>\nLanguage-Team: Spanish <https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/>\nLanguage: es\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\nPlural-Forms: nplurals=2; plural=n != 1;\nX-Generator: Weblate 5.12.2\n"
#: cli/app/cp.go:38
msgid ""
@@ -112,18 +100,7 @@ msgid ""
"\n"
" # 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"
"\n"
" # Listar las aplicaciones instaladas en un servidor específico\n"
" abra aplicacion listar -s 1312.net\n"
"\n"
" # Listar aplicaciones instaladas en servidor específico con detalles\n"
" abra aplicacion listar -s 1312.net -S\n"
"\n"
" # Listar en qué servidores está desplegada la aplicación \"gitea\"\n"
" abra aplicacion listar -r gitea"
msgstr " # Listar todas las aplicaciones instaladas con mayor detalle\n abra aplicacion listar --estado\n\n # Listar todas las aplicaciones \"wordpress\" (puedes probar otras recetas)\n abra aplicacion listar --receta wordpress\n\n # Listar todas las aplicaciones \"wordpress\" con mayor detalle\n abra aplicacion listar --receta wordpress --estado\n\n # Listar todas las aplicaciones instaladas con mayor detalle usando los comandos abreviados\n abra app ls -S"
#: cli/app/move.go:59
msgid ""
@@ -513,7 +490,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
@@ -1107,10 +1084,7 @@ msgid ""
"Generate a report of all managed apps.\n"
"\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"
"Usa la opción \"--estado/-S\" para consultar en todos los servidores sobre el estado de despliegue."
msgstr "BIENVENIDE AL MANUAL DE USO COMANDO \"LISTAR\"\n\nEl comando \"abra aplicacion listar\" permite consultar de forma rápida qué aplicaciones están siendo gestionadas. \nAl ejecutarlo, Abra genera un informe con la lista de aplicaciones disponibles y la información básica de cada una.\nPuedes imaginar el comando listar como una forma de decirle a Abra: «muéstrame qué aplicaciones tengo»\n\nLos comandos pueden aceptar opciones y flags que permiten modificar o ampliar la información que muestran, \nen este caso es bastante útil el uso de \"--estado\". Estas opciones se escriben después del comando siguiendo \nla sintaxis detallada más adelante."
#: cli/app/new.go:321
msgid "Generate app secrets?"
@@ -2024,33 +1998,7 @@ msgid ""
" {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}\n"
"\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"
"\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"
"\n"
"Temas de ayuda adicionales:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}\n"
" {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}\n"
"\n"
"Use \"{{.CommandPath}} [command] --help\" para más información sobre un comando.{{end}}\n"
msgstr "Sintaxis del comando:{{if .Runnable}}\n {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}\n {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}\n\nComando y comando abreviado:\n {{.NameAndAliases}}{{end}}{{if .HasExample}}\n\nUSO DE\"OPCIONES\" y \"FLAGS\":\nImportante: 1312.net es únicamente un ejemplo utilizado para mostrar cómo debe configurarse este campo. No es necesario utilizar este nombre.\nAl ejecutar el comando, reemplaza 1312.net por el nombre o dominio real de tu aplicación.\n\n{{.Example}}{{end}}{{if .HasAvailableSubCommands}}\n\nComandos disponibles:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name \"help\"))}}\n {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\n\nOpciones:\n{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\n\nOpciones globales:\n{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}\n\nTemas de ayuda adicionales:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}\n {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}\n\nUse \"{{.CommandPath}} [command] --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."
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3.8"
services:
app:
image: nginx:1.31.2
image: nginx:1.31.4
secrets:
- test_pass_one
- test_pass_two
-30
View File
@@ -1,30 +0,0 @@
#!/usr/bin/env bash
# This file generates local DNS aliases for the client VM for integration testing
echo "generating hosts file..."
hosts_file_path="./tests/resources/extra_hosts"
echo "removing old file..."
rm -f $hosts_file_path
# the server domain
server_ip="10.0.0.3"
domain="abra.local"
echo "$server_ip $domain" >> $hosts_file_path
# static subdomains used in integration tests
subdomains=("gitea" "zammad" "custom-html" "foo" "foobar")
for subdomain in "${subdomains[@]}"
do
echo "$server_ip $subdomain.$domain" >> $hosts_file_path
done
search_dir=./tests/integration
for entry in "$search_dir"/*.bats
do
file_name=$(basename "${entry}")
# converting file names to testing subdomains
subdomain=$(echo "$file_name" | tr . _)
echo "$server_ip $subdomain.$domain" >> $hosts_file_path
done
echo "writing extra hosts success"
+1 -2
View File
@@ -7,5 +7,4 @@
* Integration tests are in `./tests/integration`. Please see [these
docs](https://docs.coopcloud.tech/abra/hack/#integration-tests) for
instructions and tips on how to run them. If you want to run it locally via nix,
read the [nix testing docs](../nix/hosts/README.md).
instructions and tips on how to run them.
+5 -9
View File
@@ -598,7 +598,8 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
assert_success
_inspect_env_test_app
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
assert_success
assert_output --partial "WITH_COMMENT=foo"
@@ -609,7 +610,8 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --force
assert_success
_inspect_env_test_app
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
assert_success
refute_output --partial "WITH_COMMENT=foo"
assert_output --partial "WITH_COMMENT=bar"
@@ -627,14 +629,8 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
assert_success
if _is_local;
then
run docker service inspect --format "{{ range .Endpoint.Ports }}{{ .Protocol }}={{ .PublishedPort }}{{ end }}" \
run docker service inspect --format '{{ range .Endpoint.Ports }}{{ .Protocol }}={{ .PublishedPort }}{{ end }}' \
"${TEST_APP_DOMAIN//./_}_app"
else
run ssh "$TEST_SERVER" "docker service inspect --format '{{ range .Endpoint.Ports }}{{ .Protocol }}={{ .PublishedPort }}{{ end }}' ${TEST_APP_DOMAIN//./_}_app"
fi
assert_success
assert_output --partial "tcp=1312"
assert_output --partial "udp=1312"
@@ -133,7 +133,8 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input
assert_success
_inspect_env_test_app
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
assert_success
assert_output --partial "$TEST_RECIIPE:0.1.0+1.20.0"
}
+4 -11
View File
@@ -140,22 +140,15 @@ teardown(){
_undeploy_app
sanitisedDomainName="${TEST_APP_DOMAIN//./_}"
remote_ssh_command=""
if _is_local;
then
run docker config create "${sanitisedDomainName}_test_conf_v99" "$ABRA_DIR/recipes/abra-test-recipe/abra.sh"
else
remote_ssh_command="ssh '$TEST_SERVER'"
run cat "$ABRA_DIR/recipes/abra-test-recipe/abra.sh" | ssh "$TEST_SERVER" docker config create "${sanitisedDomainName}_test_conf_v99 -"
assert_success
fi
run docker config create "${sanitisedDomainName}_test_conf_v99" "$ABRA_DIR/recipes/abra-test-recipe/abra.sh"
assert_success
assert bash -c "$remote_ssh_command docker config ls | grep -q test_conf_v99"
assert bash -c "docker config ls | grep -q test_conf_v99"
run $ABRA app rm "$TEST_APP_DOMAIN" --no-input
assert_success
refute bash -c "$remote_ssh_command docker config ls | grep -q test_conf_v99"
refute bash -c "docker config ls | grep -q test_conf_v99"
}
@test "remove .env file" {
@@ -54,7 +54,8 @@ teardown(){
--no-input
assert_success
_inspect_env_test_app
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
assert_success
assert_output --partial "$TEST_RECIIPE:0.1.0+1.20.0"
}
@@ -53,7 +53,8 @@ teardown(){
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input
assert_success
_inspect_env_test_app
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
assert_success
assert_output --partial "$TEST_RECIPE:0.2.0+1.21.0"
assert_output --partial "$TEST_RECIIPE:0.2.0+1.21.0"
}
+2 -11
View File
@@ -31,18 +31,9 @@ _ensure_ssh_agent() {
exit 1
fi
if [[ ! -v SSH_AUTH_SOCK ]]; then
export SSH_AUTH_SOCK="$HOME/.ssh/ssh_auth_sock"
export SSH_AUTH_SOCK="$HOME/.ssh/ssh_auth_sock"
if [ ! -S ~/.ssh/ssh_auth_sock ]; then
eval `ssh-agent`
ln -sf "$SSH_AUTH_SOCK" ~/.ssh/ssh_auth_sock
fi
}
_is_local() {
if [[ "$TEST_SERVER" == "default" ]];
then
return 0
else
return 1
fi
}
+6 -30
View File
@@ -1,37 +1,13 @@
#!/usr/bin/env bash
_ensure_swarm() {
if [ "$(docker info | grep Swarm | sed 's/Swarm: //g' | tr -d ' ')" == "inactive" ]; then
run docker swarm init --advertise-addr 127.0.0.1:2377
assert_success
if [ "$(docker info | grep Swarm | sed 's/Swarm: //g' | tr -d ' ')" == "inactive" ]; then
run docker swarm init --advertise-addr 127.0.0.1:2377
assert_success
fi
if ! docker network ls | grep -q 'proxy'; then
run docker network create -d overlay proxy
assert_success
if ! $(docker network ls | grep -q 'proxy'); then
run docker network create -d overlay proxy
assert_success
fi
if ! _is_local;
then
if [ "$(ssh "$TEST_SERVER" docker info | grep Swarm | sed 's/Swarm: //g' | tr -d ' ')" == "inactive" ]; then
run ssh "$TEST_SERVER" docker swarm init --advertise-addr 127.0.0.1:2377
assert_success
fi
if ! ssh "$TEST_SERVER" docker network ls | grep -q 'proxy'; then
run ssh "$TEST_SERVER" docker network create -d overlay proxy
assert_success
fi
fi
}
_inspect_env_test_app() {
if _is_local;
then
containerId="$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)"
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' "$containerId"
else
containerId="$(ssh "$TEST_SERVER" docker ps -f name="$TEST_APP_DOMAIN" -q)"
run ssh "$TEST_SERVER" "docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' '$containerId'"
fi
}
+2
View File
@@ -38,6 +38,8 @@ teardown(){
run $ABRA recipe upgrade "custom-html" --no-input
assert_success
assert_output --partial 'can upgrade service: app'
assert_exists "$ABRA_DIR/recipes/custom-html"
}
+3 -3
View File
@@ -27,7 +27,7 @@ teardown(){
assert bash -c "docker context ls | grep -q $TEST_SERVER"
server_dir_perms=$(stat -c "%a" "$ABRA_DIR/servers/$TEST_SERVER")
assert_equal "$server_dir_perms" "700"
assert_equal $server_dir_perms "700"
}
@test "error if using name and --local together" {
@@ -43,8 +43,8 @@ teardown(){
assert bash -c "docker context ls | grep -q default"
assert_output --partial 'local server successfully added'
server_dir_perms=$(stat -c "%a" "$ABRA_DIR/servers/default")
assert_equal "$server_dir_perms" "700"
server_dir_perms=$(stat -c "%a" "$ABRA_DIR/servers/$TEST_SERVER")
assert_equal $server_dir_perms "700"
}
@test "create local server fails when no docker swarm" {
-57
View File
@@ -1,57 +0,0 @@
10.0.0.3 abra.local
10.0.0.3 gitea.abra.local
10.0.0.3 zammad.abra.local
10.0.0.3 custom-html.abra.local
10.0.0.3 foo.abra.local
10.0.0.3 foobar.abra.local
10.0.0.3 app_check_bats.abra.local
10.0.0.3 app_cmd_bats.abra.local
10.0.0.3 app_config_bats.abra.local
10.0.0.3 app_cp_bats.abra.local
10.0.0.3 app_deploy_bats.abra.local
10.0.0.3 app_deploy_env_version_bats.abra.local
10.0.0.3 app_deploy_overview_bats.abra.local
10.0.0.3 app_deploy_remote_recipes_bats.abra.local
10.0.0.3 app_env_bats.abra.local
10.0.0.3 app_env_version_bats.abra.local
10.0.0.3 app_labels_bats.abra.local
10.0.0.3 app_list_bats.abra.local
10.0.0.3 app_logs_bats.abra.local
10.0.0.3 app_move_bats.abra.local
10.0.0.3 app_new_bats.abra.local
10.0.0.3 app_ps_bats.abra.local
10.0.0.3 app_remove_bats.abra.local
10.0.0.3 app_restart_bats.abra.local
10.0.0.3 app_rollback_bats.abra.local
10.0.0.3 app_rollback_env_version_bats.abra.local
10.0.0.3 app_rollback_overview_bats.abra.local
10.0.0.3 app_run_bats.abra.local
10.0.0.3 app_secret_bats.abra.local
10.0.0.3 app_secret_env_version_bats.abra.local
10.0.0.3 app_services_bats.abra.local
10.0.0.3 app_undeploy_bats.abra.local
10.0.0.3 app_undeploy_env_version_bats.abra.local
10.0.0.3 app_undeploy_overview_bats.abra.local
10.0.0.3 app_upgrade_bats.abra.local
10.0.0.3 app_upgrade_env_version_bats.abra.local
10.0.0.3 app_upgrade_overview_bats.abra.local
10.0.0.3 app_volume_bats.abra.local
10.0.0.3 autocomplete_bats.abra.local
10.0.0.3 catalogue_bats.abra.local
10.0.0.3 dirs_bats.abra.local
10.0.0.3 install_bats.abra.local
10.0.0.3 recipe_diff_bats.abra.local
10.0.0.3 recipe_fetch_bats.abra.local
10.0.0.3 recipe_lint_bats.abra.local
10.0.0.3 recipe_list_bats.abra.local
10.0.0.3 recipe_new_bats.abra.local
10.0.0.3 recipe_release_bats.abra.local
10.0.0.3 recipe_reset_bats.abra.local
10.0.0.3 recipe_upgrade_bats.abra.local
10.0.0.3 recipe_version_bats.abra.local
10.0.0.3 server_add_bats.abra.local
10.0.0.3 server_list_bats.abra.local
10.0.0.3 server_prune_bats.abra.local
10.0.0.3 server_remove_bats.abra.local
10.0.0.3 upgrade_bats.abra.local
10.0.0.3 version_bats.abra.local
-7
View File
@@ -1,7 +0,0 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBQ967UhxmFkq71WXvVRkEmtehCGsEnBVppAXxgtHHm7wAAAJhFGwTnRRsE
5wAAAAtzc2gtZWQyNTUxOQAAACBQ967UhxmFkq71WXvVRkEmtehCGsEnBVppAXxgtHHm7w
AAAEAnHEZcf2NeRQEcJC/aVgUWsdOz+vQgEG9ZY+3ErCeaKFD3rtSHGYWSrvVZe9VGQSa1
6EIawScFWmkBfGC0cebvAAAAFWFicmEgaW50ZWdyYXRpb24gdGVzdA==
-----END OPENSSH PRIVATE KEY-----
@@ -1 +0,0 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFD3rtSHGYWSrvVZe9VGQSa16EIawScFWmkBfGC0cebv abra integration test
-1
View File
@@ -1 +0,0 @@
/path/to/this/repo
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3.8"
services:
app:
image: nginx:1.31.2
image: nginx:1.31.4
networks:
- proxy
deploy:
+3 -3
View File
@@ -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
View File
@@ -5,3 +5,4 @@ profile.out
.tmp/
.git-dist/
.vscode
build/tools/
+42
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
FROM golang:1.26@sha256:f96cc555eb8db430159a3aa6797cd5bae561945b7b0fe7d0e284c63a3b291609
FROM golang:1.27@sha256:eb37f58646a901dc7727cf448cae36daaefaba79de33b5058dab79aa4c04aefb
ENV GOOS=linux
ENV GOARCH=arm
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.26@sha256:f96cc555eb8db430159a3aa6797cd5bae561945b7b0fe7d0e284c63a3b291609
FROM golang:1.27@sha256:eb37f58646a901dc7727cf448cae36daaefaba79de33b5058dab79aa4c04aefb
ENV GOOS=linux
ENV GOARCH=arm64
+20 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
@@ -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.
@@ -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
View File
@@ -0,0 +1,12 @@
go-spew
=======
[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](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.
@@ -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
@@ -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
@@ -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: " "}
}
@@ -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
@@ -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.
+82 -96
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 (

Some files were not shown because too many files have changed in this diff Show More