Compare commits

..

10 Commits

Author SHA1 Message Date
renovate-bot 324993fd25 chore(deps): update dependency codespell to v2.4.3
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-07-29 13:01:17 +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
110 changed files with 8455 additions and 25892 deletions
-35
View File
@@ -84,38 +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:
sudo microvm -f git+file://$$(pwd) -c $(CLIENT_VM)
vm-client-start:
sudo systemctl start microvm@$(CLIENT_VM).service
vm-client-update:
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)
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-stop
sudo rm -rf /var/lib/microvms/$(SERVER_VM)
test-integration:
ssh-keygen -t ed25519 -f ./test-integration -q -N "" -C "abra integration test"
Generated
-60
View File
@@ -18,48 +18,6 @@
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1785119570,
"narHash": "sha256-Rgs2xKnGLFWQscxUaXX07oyZeuMDOHEbqDOsgliLFGM=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "d4fd24667c8cbef124bb70a20380cab75ec8474d",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-26.05",
"repo": "home-manager",
"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,
@@ -79,27 +37,9 @@
"root": {
"inputs": {
"flake-utils": "flake-utils",
"home-manager": "home-manager",
"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,
+20 -56
View File
@@ -3,71 +3,35 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
home-manager = {
url = "github:nix-community/home-manager/release-26.05";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
microvm = {
url = "github:astro/microvm.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
self,
nixpkgs,
microvm,
home-manager,
flake-utils,
}:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
publicKey = builtins.readFile ./test-integration.pub;
privateKey = builtins.readFile ./test-integration;
in
{
nixosConfigurations = {
abra-client = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
microvm.nixosModules.microvm
home-manager.nixosModules.home-manager
./nix/hosts/client/configuration.nix
];
specialArgs = { inherit publicKey privateKey; };
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; };
apps = rec {
abra = flake-utils.lib.mkApp { drv = self.packages.${system}.abra; };
default = abra;
};
};
packages = rec {
abra = pkgs.callPackage ./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
];
};
};
devShells.default = pkgs.mkShell {
packages = with pkgs; [
go_1_26
gnumake
];
};
}
);
}
+6 -6
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
)
@@ -124,10 +124,10 @@ require (
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
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
@@ -155,7 +155,7 @@ require (
github.com/stretchr/testify v1.11.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
+12 -12
View File
@@ -393,8 +393,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=
@@ -966,8 +966,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 +1043,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 +1139,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 +1155,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=
-162
View File
@@ -1,162 +0,0 @@
{
lib,
pkgs,
publicKey,
privateKey,
...
}:
let
index = 2;
mac = "00:00:00:00:00:01";
serverIp = "10.0.0.3";
in
{
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 = "/home/dev/Documents/abra";
mountPoint = "/home/dev/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 = [
# Quad9.net
"9.9.9.9"
"149.112.112.112"
"2620:fe::fe"
"2620:fe::9"
];
};
};
# open the ports to allow ssh access from the host
networking.firewall.allowedTCPPorts = [ 22 ];
networking.firewall.allowedUDPPorts = [ 22 ];
networking.extraHosts = ''
${serverIp} abra.local
${serverIp} app_check_bats.abra.local
'';
environment.variables = {
CGO_ENABLED = 0;
TEST_SERVER = "abra.local";
ABRA_DIR = "$HOME/.abra_test";
};
users.users.dev = {
isNormalUser = true;
password = "test";
home = "/home/dev";
description = "Alice Foobar";
extraGroups = [
"wheel"
"networkmanager"
];
openssh.authorizedKeys.keys = [
publicKey
];
};
# ssh agent with askpass
programs.ssh = {
startAgent = true;
};
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
AllowUsers = [ "dev" ];
};
};
programs.git.enable = true;
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
];
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
users.dev =
{ ... }:
{
imports = [
./home.nix
];
};
# pass args to home-manager
extraSpecialArgs = {
publicKey = publicKey;
privateKey = privateKey;
serverIp = serverIp;
};
};
}
-41
View File
@@ -1,41 +0,0 @@
{
serverIp,
...
}:
{
# Home Manager needs a bit of information about you and the
# paths it should manage.
home.username = "dev";
home.homeDirectory = "/home/dev";
# 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 = "dev";
Port = 22;
IdentityFile = "/home/dev/abra/test-integration";
};
"*.abra.local" = {
HostName = serverIp;
User = "dev";
Port = 22;
IdentityFile = "/home/dev/abra/test-integration";
};
};
};
}
-116
View File
@@ -1,116 +0,0 @@
{
lib,
publicKey,
...
}:
let
index = 3;
mac = "00:00:00:00:00:02";
clientIp = "10.0.0.2";
in
{
microvm = {
vcpu = 2;
mem = 2049; # 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 = "2G";
};
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 = [
# Quad9.net
"9.9.9.9"
"149.112.112.112"
"2620:fe::fe"
"2620:fe::9"
];
};
};
# open the ports to allow ssh access from the host
networking.firewall.allowedTCPPorts = [
22
80
443
];
networking.firewall.allowedUDPPorts = [
22
80
443
];
users.users.dev = {
isNormalUser = true;
password = "test";
home = "/home/dev";
description = "Alice Foobar";
extraGroups = [
"wheel"
"networkmanager"
"docker"
];
openssh.authorizedKeys.keys = [
publicKey
];
};
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
AllowUsers = [ "dev" ];
};
};
virtualisation.docker = {
enable = true;
liveRestore = false;
};
}
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3.8"
services:
app:
image: nginx:1.31.2
image: nginx:1.31.3
secrets:
- test_pass_one
- test_pass_two
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3.8"
services:
app:
image: nginx:1.31.2
image: nginx:1.31.3
networks:
- proxy
deploy:
+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.25@sha256:31c1e53dfc1cc2d269deec9c83f58729fa3c53dc9a576f6426109d1e319e9e9a
ENV GOOS=linux
ENV GOARCH=arm
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.26@sha256:f96cc555eb8db430159a3aa6797cd5bae561945b7b0fe7d0e284c63a3b291609
FROM golang:1.25@sha256:31c1e53dfc1cc2d269deec9c83f58729fa3c53dc9a576f6426109d1e319e9e9a
ENV GOOS=linux
ENV GOARCH=arm64
+1 -1
View File
@@ -1 +1 @@
codespell==2.4.2
codespell==2.4.3
-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 (
+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 "math"
+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 (
-13
View File
@@ -1,13 +0,0 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
package idna
// Transitional processing is disabled by default in Go 1.18.
// https://golang.org/issue/47510
const transitionalLookup = false
@@ -4,8 +4,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.10
// Package idna implements IDNA2008 using the compatibility processing
// defined by UTS (Unicode Technical Standard) #46, which defines a standard to
// deal with the transition from IDNA2003.
@@ -20,6 +18,7 @@ package idna // import "golang.org/x/net/idna"
import (
"fmt"
"strings"
"unicode"
"unicode/utf8"
"golang.org/x/text/secure/bidirule"
@@ -27,6 +26,8 @@ import (
"golang.org/x/text/unicode/norm"
)
const unicode16 = unicode.Version >= "16.0.0"
// NOTE: Unlike common practice in Go APIs, the functions will return a
// sanitized domain name in case of errors. Browsers sometimes use a partially
// evaluated string as lookup.
@@ -101,6 +102,11 @@ func ValidateLabels(enable bool) Option {
}
}
// validateLabels reports whether the ValidateLabels option is enabled.
func (p *Profile) validateLabels() bool {
return p.fromPuny != nil
}
// CheckHyphens sets whether to check for correct use of hyphens ('-') in
// labels. Most web browsers do not have this option set, since labels such as
// "r3---sn-apo3qvuoxuxbt-j5pe" are in common use.
@@ -263,6 +269,10 @@ func (p *Profile) String() string {
return s
}
// Transitional processing is disabled by default as of Go 1.18.
// https://golang.org/issue/47510
const transitionalLookup = false
var (
// Punycode is a Profile that does raw punycode processing with a minimum
// of validation.
@@ -324,15 +334,30 @@ func (e labelError) Error() string {
return fmt.Sprintf("idna: invalid label %q", e.label)
}
type runeError rune
func (e runeError) code() string { return "P1" }
func (e runeError) Error() string {
return fmt.Sprintf("idna: disallowed rune %U", e)
type runeError struct {
r rune
code_ string
}
// process implements the algorithm described in section 4 of UTS #46,
// see https://www.unicode.org/reports/tr46.
func (e runeError) code() string { return e.code_ }
func (e runeError) Error() string {
return fmt.Sprintf("idna: disallowed rune %U", e.r)
}
// code16 returns old for Unicode < 16, new for Unicode >= 16.
func code16(old, new string) string {
if unicode16 {
return new
}
return old
}
// process10 implements the algorithm described in section 4 of UTS #46.
// It implements both the Unicode 10 algorithm
// (https://www.unicode.org/reports/tr46/tr46-19.html)
// and the Unicode 16 algorithm
// (https://www.unicode.org/reports/tr46/tr46-35.html)
// depending on unicode16, which in turn depends on unicode.Version.
func (p *Profile) process(s string, toASCII bool) (string, error) {
var err error
var isBidi bool
@@ -347,8 +372,12 @@ func (p *Profile) process(s string, toASCII bool) (string, error) {
// TODO: allow for a quick check of the tables data.
// It seems like we should only create this error on ToASCII, but the
// UTS 46 conformance tests suggests we should always check this.
labelCode := "X4_2"
if !unicode16 || toASCII {
labelCode = "A4"
}
if err == nil && p.verifyDNSLength && s == "" {
err = &labelError{s, "A4"}
err = labelError{s, labelCode}
}
labels := labelIter{orig: s}
for ; !labels.done(); labels.next() {
@@ -357,12 +386,13 @@ func (p *Profile) process(s string, toASCII bool) (string, error) {
// Empty labels are not okay. The label iterator skips the last
// label if it is empty.
if err == nil && p.verifyDNSLength {
err = &labelError{s, "A4"}
err = labelError{s, labelCode}
}
continue
}
if strings.HasPrefix(label, acePrefix) {
u, err2 := decode(label[len(acePrefix):])
enc := label[len(acePrefix):]
u, err2 := decode(enc)
if err2 != nil {
if err == nil {
err = err2
@@ -370,6 +400,9 @@ func (p *Profile) process(s string, toASCII bool) (string, error) {
// Spec says keep the old label.
continue
}
if unicode16 && err == nil && len(u) > 0 && isASCII(u) {
err = punyError(enc)
}
isBidi = isBidi || bidirule.DirectionString(u) != bidi.LeftToRight
labels.set(u)
if err == nil && p.fromPuny != nil {
@@ -379,16 +412,16 @@ func (p *Profile) process(s string, toASCII bool) (string, error) {
// This should be called on NonTransitional, according to the
// spec, but that currently does not have any effect. Use the
// original profile to preserve options.
err = p.validateLabel(u)
err = p.validateLabel(u, labelCode)
}
} else if err == nil {
err = p.validateLabel(label)
err = p.validateLabel(label, labelCode)
}
}
if isBidi && p.bidirule != nil && err == nil {
for labels.reset(); !labels.done(); labels.next() {
if !p.bidirule(labels.label()) {
err = &labelError{s, "B"}
err = labelError{s, "B"}
break
}
}
@@ -406,24 +439,36 @@ func (p *Profile) process(s string, toASCII bool) (string, error) {
}
n := len(label)
if p.verifyDNSLength && err == nil && (n == 0 || n > 63) {
err = &labelError{label, "A4"}
err = labelError{label, labelCode}
}
}
}
s = labels.result()
if toASCII && p.verifyDNSLength && err == nil {
if unicode16 && strings.HasSuffix(s, ".") {
err = labelError{s, labelCode}
}
// Compute the length of the domain name minus the root label and its dot.
n := len(s)
if n > 0 && s[n-1] == '.' {
n--
}
if len(s) < 1 || n > 253 {
err = &labelError{s, "A4"}
err = labelError{s, labelCode}
}
}
return s, err
}
func isASCII(s string) bool {
for _, c := range []byte(s) {
if c >= 0x80 {
return false
}
}
return true
}
func normalize(p *Profile, s string) (mapped string, isBidi bool, err error) {
// TODO: consider first doing a quick check to see if any of these checks
// need to be done. This will make it slower in the general case, but
@@ -436,12 +481,12 @@ func normalize(p *Profile, s string) (mapped string, isBidi bool, err error) {
func validateRegistration(p *Profile, s string) (idem string, bidi bool, err error) {
// TODO: filter need for normalization in loop below.
if !norm.NFC.IsNormalString(s) {
return s, false, &labelError{s, "V1"}
return s, false, labelError{s, "V1"}
}
for i := 0; i < len(s); {
v, sz := trie.lookupString(s[i:])
if sz == 0 {
return s, bidi, runeError(utf8.RuneError)
return s, bidi, runeError{utf8.RuneError, "P1"}
}
bidi = bidi || info(v).isBidi(s[i:])
// Copy bytes not copied so far.
@@ -449,9 +494,12 @@ func validateRegistration(p *Profile, s string) (idem string, bidi bool, err err
// TODO: handle the NV8 defined in the Unicode idna data set to allow
// for strict conformance to IDNA2008.
case valid, deviation:
if sz == 1 && p.useSTD3Rules && !allowedSTD3(rune(s[i])) {
return s, bidi, runeError{rune(s[i]), "P1"}
}
case disallowed, mapped, unknown, ignored:
r, _ := utf8.DecodeRuneInString(s[i:])
return s, bidi, runeError(r)
return s, bidi, runeError{r, "P1"}
}
i += sz
}
@@ -489,7 +537,7 @@ func validateAndMap(p *Profile, s string) (vm string, bidi bool, err error) {
b = append(b, "\ufffd"...)
k = len(s)
if err == nil {
err = runeError(utf8.RuneError)
err = runeError{utf8.RuneError, "P1"}
}
break
}
@@ -502,14 +550,26 @@ func validateAndMap(p *Profile, s string) (vm string, bidi bool, err error) {
case valid:
continue
case disallowed:
if err == nil {
// Unicode 16 delays the error until validateLabels.
// Unicode 10 gave an error now.
if !unicode16 && err == nil {
r, _ := utf8.DecodeRuneInString(s[start:])
err = runeError(r)
err = runeError{r, "P1"}
}
continue
case mapped, deviation:
case deviation:
if unicode16 && !p.transitional {
break
}
fallthrough
case mapped:
b = append(b, s[k:start]...)
b = info(v).appendMapping(b, s[start:i])
// Unicode 16 requires a special case to handle ẞ -> ss in transitional mode.
if unicode16 && p.transitional && s[start:start+sz] == "ẞ" {
b = append(b, "ss"...)
} else {
b = info(v).appendMapping(b, s[start:i])
}
case ignored:
b = append(b, s[k:start]...)
// drop the rune
@@ -600,13 +660,13 @@ const acePrefix = "xn--"
func (p *Profile) simplify(cat category) category {
switch cat {
case disallowedSTD3Mapped:
case disallowedSTD3Mapped: // only happens for pre-Unicode 16
if p.useSTD3Rules {
cat = disallowed
} else {
cat = mapped
}
case disallowedSTD3Valid:
case disallowedSTD3Valid: // only happens for pre-Unicode 16
if p.useSTD3Rules {
cat = disallowed
} else {
@@ -625,17 +685,18 @@ func (p *Profile) simplify(cat category) category {
func validateFromPunycode(p *Profile, s string) error {
if !norm.NFC.IsNormalString(s) {
return &labelError{s, "V1"}
return labelError{s, "V1"}
}
// TODO: detect whether string may have to be normalized in the following
// loop.
for i := 0; i < len(s); {
v, sz := trie.lookupString(s[i:])
if sz == 0 {
return runeError(utf8.RuneError)
return runeError{utf8.RuneError, "P1"}
}
if c := p.simplify(info(v).category()); c != valid && c != deviation {
return &labelError{s, "V6"}
cat := info(v).category()
if c := p.simplify(cat); c != valid && c != deviation {
return labelError{s, code16("V6", "V7")}
}
i += sz
}
@@ -704,23 +765,51 @@ var joinStates = [][numJoinTypes]joinState{
},
}
// allowedSTD3 reports whether r is a rune that can appear in a domain name
// according to STD3. We allow all non-ASCII runes and then letters, digits, hyphens.
// We also add dot so that this can be run against the whole name and not just
// a single name element (label). The surrounding code checks dots well enough.
func allowedSTD3(r rune) bool {
return r >= 0x80 || 'a' <= r && r <= 'z' || '0' <= r && r <= '9' || r == '-' || r == '.'
}
// validateLabel validates the criteria from Section 4.1. Item 1, 4, and 6 are
// already implicitly satisfied by the overall implementation.
func (p *Profile) validateLabel(s string) (err error) {
func (p *Profile) validateLabel(s string, labelCode string) (err error) {
if s == "" {
if p.verifyDNSLength {
return &labelError{s, "A4"}
return labelError{s, labelCode}
}
return nil
}
if p.checkHyphens {
if len(s) > 4 && s[2] == '-' && s[3] == '-' {
return &labelError{s, "V2"}
return labelError{s, "V2"}
}
if s[0] == '-' || s[len(s)-1] == '-' {
return &labelError{s, "V3"}
return labelError{s, "V3"}
}
}
// Unicode 16's TR 46 delays the rune validity checks until after the label is decoded.
// (validateAndMap did not reject them earlier.)
if unicode16 && p.validateLabels() {
for i := 0; i < len(s); {
v, sz := trie.lookupString(s[i:])
if sz == 0 {
return runeError{utf8.RuneError, "P1"}
}
cat := info(v).category()
if c := p.simplify(cat); c != valid && (!p.transitional || c != deviation) {
return labelError{s, "V7"}
}
if sz == 1 && p.useSTD3Rules && !allowedSTD3(rune(s[i])) {
return runeError{rune(s[i]), "U1"}
}
i += sz
}
}
if !p.checkJoiners {
return nil
}
@@ -729,7 +818,7 @@ func (p *Profile) validateLabel(s string) (err error) {
v, sz := trie.lookupString(s)
x := info(v)
if x.isModifier() {
return &labelError{s, "V5"}
return labelError{s, code16("V5", "V6")}
}
// Quickly return in the absence of zero-width (non) joiners.
if strings.Index(s, zwj) == -1 && strings.Index(s, zwnj) == -1 {
@@ -754,8 +843,9 @@ func (p *Profile) validateLabel(s string) (err error) {
x = info(v)
}
if st == stateFAIL || st == stateAfter {
return &labelError{s, "C"}
return labelError{s, "C"}
}
return nil
}
@@ -767,3 +857,24 @@ func ascii(s string) bool {
}
return true
}
// appendMapping appends the mapping for the respective rune. isMapped must be
// true. A mapping is a categorization of a rune as defined in UTS #46.
func (c info) appendMapping(b []byte, s string) []byte {
index := int(c >> indexShift)
if c&xorBit == 0 {
p := index
return append(b, mappings[mappingIndex[p]:mappingIndex[p+1]]...)
}
b = append(b, s...)
if c&inlineXOR == inlineXOR {
// TODO: support and handle two-byte inline masks
b[len(b)-1] ^= byte(index)
} else {
for p := len(b) - int(xorData[index]); p < len(b); p++ {
index++
b[p] ^= xorData[index]
}
}
return b
}
-717
View File
@@ -1,717 +0,0 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !go1.10
// Package idna implements IDNA2008 using the compatibility processing
// defined by UTS (Unicode Technical Standard) #46, which defines a standard to
// deal with the transition from IDNA2003.
//
// IDNA2008 (Internationalized Domain Names for Applications), is defined in RFC
// 5890, RFC 5891, RFC 5892, RFC 5893 and RFC 5894.
// UTS #46 is defined in https://www.unicode.org/reports/tr46.
// See https://unicode.org/cldr/utility/idna.jsp for a visualization of the
// differences between these two standards.
package idna // import "golang.org/x/net/idna"
import (
"fmt"
"strings"
"unicode/utf8"
"golang.org/x/text/secure/bidirule"
"golang.org/x/text/unicode/norm"
)
// NOTE: Unlike common practice in Go APIs, the functions will return a
// sanitized domain name in case of errors. Browsers sometimes use a partially
// evaluated string as lookup.
// TODO: the current error handling is, in my opinion, the least opinionated.
// Other strategies are also viable, though:
// Option 1) Return an empty string in case of error, but allow the user to
// specify explicitly which errors to ignore.
// Option 2) Return the partially evaluated string if it is itself a valid
// string, otherwise return the empty string in case of error.
// Option 3) Option 1 and 2.
// Option 4) Always return an empty string for now and implement Option 1 as
// needed, and document that the return string may not be empty in case of
// error in the future.
// I think Option 1 is best, but it is quite opinionated.
// ToASCII is a wrapper for Punycode.ToASCII.
func ToASCII(s string) (string, error) {
return Punycode.process(s, true)
}
// ToUnicode is a wrapper for Punycode.ToUnicode.
func ToUnicode(s string) (string, error) {
return Punycode.process(s, false)
}
// An Option configures a Profile at creation time.
type Option func(*options)
// Transitional sets a Profile to use the Transitional mapping as defined in UTS
// #46. This will cause, for example, "ß" to be mapped to "ss". Using the
// transitional mapping provides a compromise between IDNA2003 and IDNA2008
// compatibility. It is used by some browsers when resolving domain names. This
// option is only meaningful if combined with MapForLookup.
func Transitional(transitional bool) Option {
return func(o *options) { o.transitional = transitional }
}
// VerifyDNSLength sets whether a Profile should fail if any of the IDN parts
// are longer than allowed by the RFC.
//
// This option corresponds to the VerifyDnsLength flag in UTS #46.
func VerifyDNSLength(verify bool) Option {
return func(o *options) { o.verifyDNSLength = verify }
}
// RemoveLeadingDots removes leading label separators. Leading runes that map to
// dots, such as U+3002 IDEOGRAPHIC FULL STOP, are removed as well.
func RemoveLeadingDots(remove bool) Option {
return func(o *options) { o.removeLeadingDots = remove }
}
// ValidateLabels sets whether to check the mandatory label validation criteria
// as defined in Section 5.4 of RFC 5891. This includes testing for correct use
// of hyphens ('-'), normalization, validity of runes, and the context rules.
// In particular, ValidateLabels also sets the CheckHyphens and CheckJoiners flags
// in UTS #46.
func ValidateLabels(enable bool) Option {
return func(o *options) {
// Don't override existing mappings, but set one that at least checks
// normalization if it is not set.
if o.mapping == nil && enable {
o.mapping = normalize
}
o.trie = trie
o.checkJoiners = enable
o.checkHyphens = enable
if enable {
o.fromPuny = validateFromPunycode
} else {
o.fromPuny = nil
}
}
}
// CheckHyphens sets whether to check for correct use of hyphens ('-') in
// labels. Most web browsers do not have this option set, since labels such as
// "r3---sn-apo3qvuoxuxbt-j5pe" are in common use.
//
// This option corresponds to the CheckHyphens flag in UTS #46.
func CheckHyphens(enable bool) Option {
return func(o *options) { o.checkHyphens = enable }
}
// CheckJoiners sets whether to check the ContextJ rules as defined in Appendix
// A of RFC 5892, concerning the use of joiner runes.
//
// This option corresponds to the CheckJoiners flag in UTS #46.
func CheckJoiners(enable bool) Option {
return func(o *options) {
o.trie = trie
o.checkJoiners = enable
}
}
// StrictDomainName limits the set of permissible ASCII characters to those
// allowed in domain names as defined in RFC 1034 (A-Z, a-z, 0-9 and the
// hyphen). This is set by default for MapForLookup and ValidateForRegistration,
// but is only useful if ValidateLabels is set.
//
// This option is useful, for instance, for browsers that allow characters
// outside this range, for example a '_' (U+005F LOW LINE). See
// http://www.rfc-editor.org/std/std3.txt for more details.
//
// This option corresponds to the UseSTD3ASCIIRules flag in UTS #46.
func StrictDomainName(use bool) Option {
return func(o *options) { o.useSTD3Rules = use }
}
// NOTE: the following options pull in tables. The tables should not be linked
// in as long as the options are not used.
// BidiRule enables the Bidi rule as defined in RFC 5893. Any application
// that relies on proper validation of labels should include this rule.
//
// This option corresponds to the CheckBidi flag in UTS #46.
func BidiRule() Option {
return func(o *options) { o.bidirule = bidirule.ValidString }
}
// ValidateForRegistration sets validation options to verify that a given IDN is
// properly formatted for registration as defined by Section 4 of RFC 5891.
func ValidateForRegistration() Option {
return func(o *options) {
o.mapping = validateRegistration
StrictDomainName(true)(o)
ValidateLabels(true)(o)
VerifyDNSLength(true)(o)
BidiRule()(o)
}
}
// MapForLookup sets validation and mapping options such that a given IDN is
// transformed for domain name lookup according to the requirements set out in
// Section 5 of RFC 5891. The mappings follow the recommendations of RFC 5894,
// RFC 5895 and UTS 46. It does not add the Bidi Rule. Use the BidiRule option
// to add this check.
//
// The mappings include normalization and mapping case, width and other
// compatibility mappings.
func MapForLookup() Option {
return func(o *options) {
o.mapping = validateAndMap
StrictDomainName(true)(o)
ValidateLabels(true)(o)
RemoveLeadingDots(true)(o)
}
}
type options struct {
transitional bool
useSTD3Rules bool
checkHyphens bool
checkJoiners bool
verifyDNSLength bool
removeLeadingDots bool
trie *idnaTrie
// fromPuny calls validation rules when converting A-labels to U-labels.
fromPuny func(p *Profile, s string) error
// mapping implements a validation and mapping step as defined in RFC 5895
// or UTS 46, tailored to, for example, domain registration or lookup.
mapping func(p *Profile, s string) (string, error)
// bidirule, if specified, checks whether s conforms to the Bidi Rule
// defined in RFC 5893.
bidirule func(s string) bool
}
// A Profile defines the configuration of a IDNA mapper.
type Profile struct {
options
}
func apply(o *options, opts []Option) {
for _, f := range opts {
f(o)
}
}
// New creates a new Profile.
//
// With no options, the returned Profile is the most permissive and equals the
// Punycode Profile. Options can be passed to further restrict the Profile. The
// MapForLookup and ValidateForRegistration options set a collection of options,
// for lookup and registration purposes respectively, which can be tailored by
// adding more fine-grained options, where later options override earlier
// options.
func New(o ...Option) *Profile {
p := &Profile{}
apply(&p.options, o)
return p
}
// ToASCII converts a domain or domain label to its ASCII form. For example,
// ToASCII("bücher.example.com") is "xn--bcher-kva.example.com", and
// ToASCII("golang") is "golang". If an error is encountered it will return
// an error and a (partially) processed result.
func (p *Profile) ToASCII(s string) (string, error) {
return p.process(s, true)
}
// ToUnicode converts a domain or domain label to its Unicode form. For example,
// ToUnicode("xn--bcher-kva.example.com") is "bücher.example.com", and
// ToUnicode("golang") is "golang". If an error is encountered it will return
// an error and a (partially) processed result.
func (p *Profile) ToUnicode(s string) (string, error) {
pp := *p
pp.transitional = false
return pp.process(s, false)
}
// String reports a string with a description of the profile for debugging
// purposes. The string format may change with different versions.
func (p *Profile) String() string {
s := ""
if p.transitional {
s = "Transitional"
} else {
s = "NonTransitional"
}
if p.useSTD3Rules {
s += ":UseSTD3Rules"
}
if p.checkHyphens {
s += ":CheckHyphens"
}
if p.checkJoiners {
s += ":CheckJoiners"
}
if p.verifyDNSLength {
s += ":VerifyDNSLength"
}
return s
}
var (
// Punycode is a Profile that does raw punycode processing with a minimum
// of validation.
Punycode *Profile = punycode
// Lookup is the recommended profile for looking up domain names, according
// to Section 5 of RFC 5891. The exact configuration of this profile may
// change over time.
Lookup *Profile = lookup
// Display is the recommended profile for displaying domain names.
// The configuration of this profile may change over time.
Display *Profile = display
// Registration is the recommended profile for checking whether a given
// IDN is valid for registration, according to Section 4 of RFC 5891.
Registration *Profile = registration
punycode = &Profile{}
lookup = &Profile{options{
transitional: true,
removeLeadingDots: true,
useSTD3Rules: true,
checkHyphens: true,
checkJoiners: true,
trie: trie,
fromPuny: validateFromPunycode,
mapping: validateAndMap,
bidirule: bidirule.ValidString,
}}
display = &Profile{options{
useSTD3Rules: true,
removeLeadingDots: true,
checkHyphens: true,
checkJoiners: true,
trie: trie,
fromPuny: validateFromPunycode,
mapping: validateAndMap,
bidirule: bidirule.ValidString,
}}
registration = &Profile{options{
useSTD3Rules: true,
verifyDNSLength: true,
checkHyphens: true,
checkJoiners: true,
trie: trie,
fromPuny: validateFromPunycode,
mapping: validateRegistration,
bidirule: bidirule.ValidString,
}}
// TODO: profiles
// Register: recommended for approving domain names: don't do any mappings
// but rather reject on invalid input. Bundle or block deviation characters.
)
type labelError struct{ label, code_ string }
func (e labelError) code() string { return e.code_ }
func (e labelError) Error() string {
return fmt.Sprintf("idna: invalid label %q", e.label)
}
type runeError rune
func (e runeError) code() string { return "P1" }
func (e runeError) Error() string {
return fmt.Sprintf("idna: disallowed rune %U", e)
}
// process implements the algorithm described in section 4 of UTS #46,
// see https://www.unicode.org/reports/tr46.
func (p *Profile) process(s string, toASCII bool) (string, error) {
var err error
if p.mapping != nil {
s, err = p.mapping(p, s)
}
// Remove leading empty labels.
if p.removeLeadingDots {
for ; len(s) > 0 && s[0] == '.'; s = s[1:] {
}
}
// It seems like we should only create this error on ToASCII, but the
// UTS 46 conformance tests suggests we should always check this.
if err == nil && p.verifyDNSLength && s == "" {
err = &labelError{s, "A4"}
}
labels := labelIter{orig: s}
for ; !labels.done(); labels.next() {
label := labels.label()
if label == "" {
// Empty labels are not okay. The label iterator skips the last
// label if it is empty.
if err == nil && p.verifyDNSLength {
err = &labelError{s, "A4"}
}
continue
}
if strings.HasPrefix(label, acePrefix) {
u, err2 := decode(label[len(acePrefix):])
if err2 != nil {
if err == nil {
err = err2
}
// Spec says keep the old label.
continue
}
labels.set(u)
if err == nil && p.fromPuny != nil {
err = p.fromPuny(p, u)
}
if err == nil {
// This should be called on NonTransitional, according to the
// spec, but that currently does not have any effect. Use the
// original profile to preserve options.
err = p.validateLabel(u)
}
} else if err == nil {
err = p.validateLabel(label)
}
}
if toASCII {
for labels.reset(); !labels.done(); labels.next() {
label := labels.label()
if !ascii(label) {
a, err2 := encode(acePrefix, label)
if err == nil {
err = err2
}
label = a
labels.set(a)
}
n := len(label)
if p.verifyDNSLength && err == nil && (n == 0 || n > 63) {
err = &labelError{label, "A4"}
}
}
}
s = labels.result()
if toASCII && p.verifyDNSLength && err == nil {
// Compute the length of the domain name minus the root label and its dot.
n := len(s)
if n > 0 && s[n-1] == '.' {
n--
}
if len(s) < 1 || n > 253 {
err = &labelError{s, "A4"}
}
}
return s, err
}
func normalize(p *Profile, s string) (string, error) {
return norm.NFC.String(s), nil
}
func validateRegistration(p *Profile, s string) (string, error) {
if !norm.NFC.IsNormalString(s) {
return s, &labelError{s, "V1"}
}
for i := 0; i < len(s); {
v, sz := trie.lookupString(s[i:])
// Copy bytes not copied so far.
switch p.simplify(info(v).category()) {
// TODO: handle the NV8 defined in the Unicode idna data set to allow
// for strict conformance to IDNA2008.
case valid, deviation:
case disallowed, mapped, unknown, ignored:
r, _ := utf8.DecodeRuneInString(s[i:])
return s, runeError(r)
}
i += sz
}
return s, nil
}
func validateAndMap(p *Profile, s string) (string, error) {
var (
err error
b []byte
k int
)
for i := 0; i < len(s); {
v, sz := trie.lookupString(s[i:])
start := i
i += sz
// Copy bytes not copied so far.
switch p.simplify(info(v).category()) {
case valid:
continue
case disallowed:
if err == nil {
r, _ := utf8.DecodeRuneInString(s[start:])
err = runeError(r)
}
continue
case mapped, deviation:
b = append(b, s[k:start]...)
b = info(v).appendMapping(b, s[start:i])
case ignored:
b = append(b, s[k:start]...)
// drop the rune
case unknown:
b = append(b, s[k:start]...)
b = append(b, "\ufffd"...)
}
k = i
}
if k == 0 {
// No changes so far.
s = norm.NFC.String(s)
} else {
b = append(b, s[k:]...)
if norm.NFC.QuickSpan(b) != len(b) {
b = norm.NFC.Bytes(b)
}
// TODO: the punycode converters require strings as input.
s = string(b)
}
return s, err
}
// A labelIter allows iterating over domain name labels.
type labelIter struct {
orig string
slice []string
curStart int
curEnd int
i int
}
func (l *labelIter) reset() {
l.curStart = 0
l.curEnd = 0
l.i = 0
}
func (l *labelIter) done() bool {
return l.curStart >= len(l.orig)
}
func (l *labelIter) result() string {
if l.slice != nil {
return strings.Join(l.slice, ".")
}
return l.orig
}
func (l *labelIter) label() string {
if l.slice != nil {
return l.slice[l.i]
}
p := strings.IndexByte(l.orig[l.curStart:], '.')
l.curEnd = l.curStart + p
if p == -1 {
l.curEnd = len(l.orig)
}
return l.orig[l.curStart:l.curEnd]
}
// next sets the value to the next label. It skips the last label if it is empty.
func (l *labelIter) next() {
l.i++
if l.slice != nil {
if l.i >= len(l.slice) || l.i == len(l.slice)-1 && l.slice[l.i] == "" {
l.curStart = len(l.orig)
}
} else {
l.curStart = l.curEnd + 1
if l.curStart == len(l.orig)-1 && l.orig[l.curStart] == '.' {
l.curStart = len(l.orig)
}
}
}
func (l *labelIter) set(s string) {
if l.slice == nil {
l.slice = strings.Split(l.orig, ".")
}
l.slice[l.i] = s
}
// acePrefix is the ASCII Compatible Encoding prefix.
const acePrefix = "xn--"
func (p *Profile) simplify(cat category) category {
switch cat {
case disallowedSTD3Mapped:
if p.useSTD3Rules {
cat = disallowed
} else {
cat = mapped
}
case disallowedSTD3Valid:
if p.useSTD3Rules {
cat = disallowed
} else {
cat = valid
}
case deviation:
if !p.transitional {
cat = valid
}
case validNV8, validXV8:
// TODO: handle V2008
cat = valid
}
return cat
}
func validateFromPunycode(p *Profile, s string) error {
if !norm.NFC.IsNormalString(s) {
return &labelError{s, "V1"}
}
for i := 0; i < len(s); {
v, sz := trie.lookupString(s[i:])
if c := p.simplify(info(v).category()); c != valid && c != deviation {
return &labelError{s, "V6"}
}
i += sz
}
return nil
}
const (
zwnj = "\u200c"
zwj = "\u200d"
)
type joinState int8
const (
stateStart joinState = iota
stateVirama
stateBefore
stateBeforeVirama
stateAfter
stateFAIL
)
var joinStates = [][numJoinTypes]joinState{
stateStart: {
joiningL: stateBefore,
joiningD: stateBefore,
joinZWNJ: stateFAIL,
joinZWJ: stateFAIL,
joinVirama: stateVirama,
},
stateVirama: {
joiningL: stateBefore,
joiningD: stateBefore,
},
stateBefore: {
joiningL: stateBefore,
joiningD: stateBefore,
joiningT: stateBefore,
joinZWNJ: stateAfter,
joinZWJ: stateFAIL,
joinVirama: stateBeforeVirama,
},
stateBeforeVirama: {
joiningL: stateBefore,
joiningD: stateBefore,
joiningT: stateBefore,
},
stateAfter: {
joiningL: stateFAIL,
joiningD: stateBefore,
joiningT: stateAfter,
joiningR: stateStart,
joinZWNJ: stateFAIL,
joinZWJ: stateFAIL,
joinVirama: stateAfter, // no-op as we can't accept joiners here
},
stateFAIL: {
0: stateFAIL,
joiningL: stateFAIL,
joiningD: stateFAIL,
joiningT: stateFAIL,
joiningR: stateFAIL,
joinZWNJ: stateFAIL,
joinZWJ: stateFAIL,
joinVirama: stateFAIL,
},
}
// validateLabel validates the criteria from Section 4.1. Item 1, 4, and 6 are
// already implicitly satisfied by the overall implementation.
func (p *Profile) validateLabel(s string) error {
if s == "" {
if p.verifyDNSLength {
return &labelError{s, "A4"}
}
return nil
}
if p.bidirule != nil && !p.bidirule(s) {
return &labelError{s, "B"}
}
if p.checkHyphens {
if len(s) > 4 && s[2] == '-' && s[3] == '-' {
return &labelError{s, "V2"}
}
if s[0] == '-' || s[len(s)-1] == '-' {
return &labelError{s, "V3"}
}
}
if !p.checkJoiners {
return nil
}
trie := p.trie // p.checkJoiners is only set if trie is set.
// TODO: merge the use of this in the trie.
v, sz := trie.lookupString(s)
x := info(v)
if x.isModifier() {
return &labelError{s, "V5"}
}
// Quickly return in the absence of zero-width (non) joiners.
if strings.Index(s, zwj) == -1 && strings.Index(s, zwnj) == -1 {
return nil
}
st := stateStart
for i := 0; ; {
jt := x.joinType()
if s[i:i+sz] == zwj {
jt = joinZWJ
} else if s[i:i+sz] == zwnj {
jt = joinZWNJ
}
st = joinStates[st][jt]
if x.isViramaModifier() {
st = joinStates[st][joinVirama]
}
if i += sz; i == len(s) {
break
}
v, sz = trie.lookupString(s[i:])
x = info(v)
}
if st == stateFAIL || st == stateAfter {
return &labelError{s, "C"}
}
return nil
}
func ascii(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] >= utf8.RuneSelf {
return false
}
}
return true
}
-11
View File
@@ -1,11 +0,0 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !go1.18
package idna
const transitionalLookup = true
+4 -1
View File
@@ -28,7 +28,7 @@ const (
tmin int32 = 1
)
func punyError(s string) error { return &labelError{s, "A3"} }
func punyError(s string) error { return &labelError{s, code16("A3", "P4")} }
// decode decodes a string as specified in section 6.2.
func decode(encoded string) (string, error) {
@@ -108,6 +108,9 @@ func encode(prefix, s string) (string, error) {
delta, n, bias := int32(0), initialN, initialBias
b, remaining := int32(0), int32(0)
for _, r := range s {
if unicode16 && r == 0xfffd {
return s, &labelError{s, "A3"}
}
if r < 0x80 {
b++
output = append(output, byte(r))
-4559
View File
File diff suppressed because it is too large Load Diff
-4653
View File
File diff suppressed because it is too large Load Diff
-4733
View File
File diff suppressed because it is too large Load Diff
-4959
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
//go:build go1.21
//go:build !go1.27
package idna
+5302
View File
File diff suppressed because it is too large Load Diff
-4486
View File
File diff suppressed because it is too large Load Diff
-30
View File
@@ -1,30 +0,0 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !go1.16
package idna
// appendMapping appends the mapping for the respective rune. isMapped must be
// true. A mapping is a categorization of a rune as defined in UTS #46.
func (c info) appendMapping(b []byte, s string) []byte {
index := int(c >> indexShift)
if c&xorBit == 0 {
s := mappings[index:]
return append(b, s[1:s[0]+1]...)
}
b = append(b, s...)
if c&inlineXOR == inlineXOR {
// TODO: support and handle two-byte inline masks
b[len(b)-1] ^= byte(index)
} else {
for p := len(b) - int(xorData[index]); p < len(b); p++ {
index++
b[p] ^= xorData[index]
}
}
return b
}
-30
View File
@@ -1,30 +0,0 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.16
package idna
// appendMapping appends the mapping for the respective rune. isMapped must be
// true. A mapping is a categorization of a rune as defined in UTS #46.
func (c info) appendMapping(b []byte, s string) []byte {
index := int(c >> indexShift)
if c&xorBit == 0 {
p := index
return append(b, mappings[mappingIndex[p]:mappingIndex[p+1]]...)
}
b = append(b, s...)
if c&inlineXOR == inlineXOR {
// TODO: support and handle two-byte inline masks
b[len(b)-1] ^= byte(index)
} else {
for p := len(b) - int(xorData[index]); p < len(b); p++ {
index++
b[p] ^= xorData[index]
}
}
return b
}
+8
View File
@@ -448,6 +448,14 @@ func NewServerRequest(rp ServerRequestParam) ServerRequestResult {
url_ = &url.URL{Host: rp.Authority}
requestURI = rp.Authority // mimic HTTP/1 server behavior
} else {
// "[The :path] pseudo-header field MUST NOT be empty [...]"
// https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.1-2.4.2
if rp.Path == "" || (rp.Path[0] != '/' && rp.Path != "*") {
return ServerRequestResult{
InvalidReason: "bad_path",
}
}
var err error
url_, err = url.ParseRequestURI(rp.Path)
if err != nil {
+37 -25
View File
@@ -6,38 +6,50 @@ package cpu
import "strconv"
// parseRelease parses a dot-separated version number. It follows the semver
// syntax, but allows the minor and patch versions to be elided.
// parseRelease parses a dot-separated version number from the prefix
// of rel. It returns ok=true only if at least the major and minor
// components were successfully parsed; the patch component is
// best-effort. Trailing vendor or build suffixes such as
// "-generic", "+", "_hi3535", or "-rc1" are ignored.
//
// This is a copy of the Go runtime's parseRelease from
// https://golang.org/cl/209597.
// https://golang.org/cl/209597, updated in https://golang.org/cl/781800.
func parseRelease(rel string) (major, minor, patch int, ok bool) {
// Strip anything after a dash or plus.
for i := range len(rel) {
if rel[i] == '-' || rel[i] == '+' {
rel = rel[:i]
break
// next consumes a run of decimal digits from the front of rel,
// returning the parsed value. If the digits are followed by a
// '.', it is consumed and more is set so the caller knows to
// parse another component; otherwise scanning terminates and
// the rest of rel is discarded.
next := func() (n int, more, ok bool) {
i := 0
for i < len(rel) && rel[i] >= '0' && rel[i] <= '9' {
i++
}
if i == 0 {
return 0, false, false
}
n, err := strconv.Atoi(rel[:i])
if err != nil {
return 0, false, false
}
if i < len(rel) && rel[i] == '.' {
rel = rel[i+1:]
return n, true, true
}
rel = ""
return n, false, true
}
next := func() (int, bool) {
for i := range len(rel) {
if rel[i] == '.' {
ver, err := strconv.Atoi(rel[:i])
rel = rel[i+1:]
return ver, err == nil
}
}
ver, err := strconv.Atoi(rel)
rel = ""
return ver, err == nil
var more bool
if major, more, ok = next(); !ok || !more {
return 0, 0, 0, false
}
if major, ok = next(); !ok || rel == "" {
return
if minor, more, ok = next(); !ok {
return 0, 0, 0, false
}
if minor, ok = next(); !ok || rel == "" {
return
if !more {
return major, minor, 0, true
}
patch, ok = next()
return
patch, _, _ = next()
return major, minor, patch, true
}
+1
View File
@@ -1874,6 +1874,7 @@ func Dup2(oldfd, newfd int) error {
//sys Dup3(oldfd int, newfd int, flags int) (err error)
//sysnb EpollCreate1(flag int) (fd int, err error)
//sysnb EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error)
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) = SYS_EPOLL_PWAIT
//sys Eventfd(initval uint, flags int) (fd int, err error) = SYS_EVENTFD2
//sys Exit(code int) = SYS_EXIT_GROUP
//sys Fallocate(fd int, mode uint32, off int64, len int64) (err error)
-1
View File
@@ -20,7 +20,6 @@ func setTimeval(sec, usec int64) Timeval {
// 64-bit file system and 32-bit uid calls
// (386 default is 32-bit file system and 16-bit uid).
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64_64
//sys Fchown(fd int, uid int, gid int) (err error) = SYS_FCHOWN32
//sys Fstat(fd int, stat *Stat_t) (err error) = SYS_FSTAT64
-1
View File
@@ -6,7 +6,6 @@
package unix
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error)
-1
View File
@@ -44,7 +44,6 @@ func Seek(fd int, offset int64, whence int) (newoffset int64, err error) {
// 64-bit file system and 32-bit uid calls
// (16-bit uid calls are not always supported in newer kernels)
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fchown(fd int, uid int, gid int) (err error) = SYS_FCHOWN32
//sys Fstat(fd int, stat *Stat_t) (err error) = SYS_FSTAT64
//sys Fstatat(dirfd int, path string, stat *Stat_t, flags int) (err error) = SYS_FSTATAT64
-1
View File
@@ -8,7 +8,6 @@ package unix
import "unsafe"
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) = SYS_EPOLL_PWAIT
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error)
-1
View File
@@ -8,7 +8,6 @@ package unix
import "unsafe"
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) = SYS_EPOLL_PWAIT
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstatfs(fd int, buf *Statfs_t) (err error)
-1
View File
@@ -6,7 +6,6 @@
package unix
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstatfs(fd int, buf *Statfs_t) (err error)
-1
View File
@@ -13,7 +13,6 @@ import (
func Syscall9(trap, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr, err syscall.Errno)
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Ftruncate(fd int, length int64) (err error) = SYS_FTRUNCATE64
-1
View File
@@ -11,7 +11,6 @@ import (
"unsafe"
)
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error) = SYS_FSTAT64
//sys Fstatat(dirfd int, path string, stat *Stat_t, flags int) (err error) = SYS_FSTATAT64
-1
View File
@@ -6,7 +6,6 @@
package unix
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error)
-1
View File
@@ -8,7 +8,6 @@ package unix
import "unsafe"
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) = SYS_EPOLL_PWAIT
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error)
-1
View File
@@ -10,7 +10,6 @@ import (
"unsafe"
)
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error)
-1
View File
@@ -6,7 +6,6 @@
package unix
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error)
+7 -1
View File
@@ -1359,6 +1359,7 @@ const (
FAN_UNLIMITED_MARKS = 0x20
FAN_UNLIMITED_QUEUE = 0x10
FD_CLOEXEC = 0x1
FD_PIDFS_ROOT = -0x2712
FD_SETSIZE = 0x400
FF0 = 0x0
FIB_RULE_DEV_DETACHED = 0x8
@@ -1970,6 +1971,8 @@ const (
MADV_DONTNEED = 0x4
MADV_DONTNEED_LOCKED = 0x18
MADV_FREE = 0x8
MADV_GUARD_INSTALL = 0x66
MADV_GUARD_REMOVE = 0x67
MADV_HUGEPAGE = 0xe
MADV_HWPOISON = 0x64
MADV_KEEPONFORK = 0x13
@@ -2114,7 +2117,7 @@ const (
MS_NOSEC = 0x10000000
MS_NOSUID = 0x2
MS_NOSYMFOLLOW = 0x100
MS_NOUSER = -0x80000000
MS_NOUSER = 0x80000000
MS_POSIXACL = 0x10000
MS_PRIVATE = 0x40000
MS_RDONLY = 0x1
@@ -3786,6 +3789,9 @@ const (
TCPOPT_TIMESTAMP = 0x8
TCPOPT_TSTAMP_HDR = 0x101080a
TCPOPT_WINDOW = 0x3
TCP_AO_KEYF_EXCLUDE_OPT = 0x2
TCP_AO_KEYF_IFINDEX = 0x1
TCP_AO_MAXKEYLEN = 0x50
TCP_CC_INFO = 0x1a
TCP_CM_INQ = 0x24
TCP_CONGESTION = 0xd
+17
View File
@@ -700,6 +700,23 @@ func EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_PWAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Eventfd(initval uint, flags int) (fd int, err error) {
r0, _, e1 := Syscall(SYS_EVENTFD2, uintptr(initval), uintptr(flags), 0)
fd = int(r0)
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64_64, uintptr(fd), uintptr(offset), uintptr(offset>>32), uintptr(length), uintptr(length>>32), uintptr(advice))
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -213,23 +213,6 @@ func sendmsg(s int, msg *Msghdr, flags int) (n int, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fchown(fd int, uid int, gid int) (err error) {
_, _, e1 := Syscall(SYS_FCHOWN32, uintptr(fd), uintptr(uid), uintptr(gid))
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_PWAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_PWAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall9(SYS_FADVISE64, uintptr(fd), 0, uintptr(offset>>32), uintptr(offset), uintptr(length>>32), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall9(SYS_FADVISE64, uintptr(fd), 0, uintptr(offset), uintptr(offset>>32), uintptr(length), uintptr(length>>32), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fchown(fd int, uid int, gid int) (err error) {
_, _, e1 := Syscall(SYS_FCHOWN, uintptr(fd), uintptr(uid), uintptr(gid))
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_PWAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {

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