diff --git a/go.mod b/go.mod index 152787412a..0dd6e61a5c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( charm.land/bubbles/v2 v2.1.1 charm.land/bubbletea/v2 v2.0.8 charm.land/lipgloss/v2 v2.0.5 - charm.land/wish/v2 v2.0.1 + charm.land/wish/v2 v2.0.3 github.com/beevik/ntp v1.5.0 github.com/charmbracelet/colorprofile v0.4.3 github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309 @@ -28,7 +28,7 @@ require ( golang.org/x/term v0.45.0 golang.org/x/tools v0.48.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 - google.golang.org/grpc v1.82.1 + google.golang.org/grpc v1.83.0 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 google.golang.org/protobuf v1.36.11 ) @@ -42,7 +42,9 @@ require ( ) require ( + charm.land/ssh v0.4.2 // indirect github.com/antihax/optional v1.0.0 // indirect + github.com/charmbracelet/x/xpty v0.1.4 // indirect github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 // indirect github.com/gokrazy/gokapi v0.0.0-20251205165548-0927bab199d4 // indirect github.com/gokrazy/internal v0.0.0-20260625065634-6994f9152c44 // indirect @@ -50,6 +52,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.19.1 // indirect github.com/mattn/go-isatty v0.0.22 // indirect + github.com/pires/go-proxyproto v0.12.0 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect golang.org/x/crypto/x509roots/fallback v0.0.0-20260630172432-7626c5025624 // indirect @@ -65,8 +68,7 @@ require ( github.com/charmbracelet/keygen v0.5.4 // indirect github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 // indirect github.com/charmbracelet/x/ansi v0.11.7 // indirect - github.com/charmbracelet/x/conpty v0.1.1 // indirect - github.com/charmbracelet/x/errors v0.0.0-20251110184232-6ab307057ac7 // indirect + github.com/charmbracelet/x/conpty v0.2.0 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect @@ -97,12 +99,12 @@ require ( github.com/sirupsen/logrus v1.9.4 github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect golang.org/x/mod v0.38.0 golang.org/x/sync v0.22.0 // indirect golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 // indirect golang.org/x/text v0.40.0 // indirect golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e65e7db0e4..2c57b821a8 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,10 @@ charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY= charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc= charm.land/log/v2 v2.0.0 h1:SY3Cey7ipx86/MBXQHwsguOT6X1exT94mmJRdzTNs+s= charm.land/log/v2 v2.0.0/go.mod h1:c3cZSRqm20qUVVAR1WmS/7ab8bgha3C6G7DjPcaVZz0= -charm.land/wish/v2 v2.0.1 h1:xYOsvQG/bYNRoKUBt0AEyMiCv9rksmCOG/fD5WifAzg= -charm.land/wish/v2 v2.0.1/go.mod h1:uBWdsKTFk9BE0JsdEv8KKF8/abBfRe/hKEGOBm3OKZA= +charm.land/ssh v0.4.2 h1:mpJW8KuCQSu5mn4L9cRtDQpVtUNa/JwbqbOHyB/H1lI= +charm.land/ssh v0.4.2/go.mod h1:so/3IECPNlYZSnE7JKn7NFmcUyyxJqIAeM4TJy35qPk= +charm.land/wish/v2 v2.0.3 h1:Xkgw31lEH9AJkPfgXYYvsgrskfDIY9ffHTxFRV4UT+4= +charm.land/wish/v2 v2.0.3/go.mod h1:i8gFfXu+IyMcGpRh6D84Wa+mDGwjYCKWcA86R+IJf0c= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= @@ -32,10 +34,8 @@ github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 h1:3FmWo github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7/go.mod h1:f/jRa757WUmaOZrbPspXymbg/GnbF+rwe4OLsG7aXYo= github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= -github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs= -github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk= -github.com/charmbracelet/x/errors v0.0.0-20251110184232-6ab307057ac7 h1:4EG8pCHK5fa8dIxv97VHC8hdkJAz6QNm1WB9BuD/WhY= -github.com/charmbracelet/x/errors v0.0.0-20251110184232-6ab307057ac7/go.mod h1:O2BTD/aMVQDmrvqroIO3fB6zXUuU07ZpVt21QTmZjRg= +github.com/charmbracelet/x/conpty v0.2.0 h1:eKtA2hm34qNfgJCDp/M6Dc0gLy7e07YEK4qAdNGOvVY= +github.com/charmbracelet/x/conpty v0.2.0/go.mod h1:fexgUnVrZgw8scD49f6VSi0Ggj9GWYIrpedRthAwW/8= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= @@ -44,6 +44,8 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8 github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/charmbracelet/x/xpty v0.1.4 h1:4jaW7u+8AHQMxesiVc+zUMsspu7GyDwtJO+gy/tFtW4= +github.com/charmbracelet/x/xpty v0.1.4/go.mod h1:7t8P7BpPiolHJ1pLzz7/4ujDbD+sUxI9yA3CBOLOIcU= github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY= github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10bQt4= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= @@ -148,6 +150,8 @@ github.com/packetcap/go-pcap v0.0.0-20251215121130-f2cf9f991e7c h1:B5gWB1LB6Oxpo github.com/packetcap/go-pcap v0.0.0-20251215121130-f2cf9f991e7c/go.mod h1:1jryUz9E2ndKwZBNHzVhLMzS3WHO0fOKydYi9XWWu9w= github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM= +github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= @@ -190,16 +194,16 @@ go.fd.io/govpp v0.13.0 h1:MnjH9I5K+X0860CeeuBcMSu3uyUKA6X9AenKzdiGpnA= go.fd.io/govpp v0.13.0/go.mod h1:MQw6XdULE9qJiqYzIUXPSVyOGWCwVLMhhFRjcf+9hmM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= @@ -210,8 +214,8 @@ golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/crypto/x509roots/fallback v0.0.0-20260630172432-7626c5025624 h1:qtosn20yWXr+4B4x23stHOMEoDZrcSR3KjGBTNKV+74= golang.org/x/crypto/x509roots/fallback v0.0.0-20260630172432-7626c5025624/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= -golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= -golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= @@ -239,10 +243,10 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdI golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= +google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 h1:rgSNvqscFZ1JgV/4wH5GOsZFSFkR2Eua9As3KIr2LlM= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2/go.mod h1:iMEtFwDlAhjDU9L5mY6U1XLwlIId/G3h+QcBHDIvrJ8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/vendor/charm.land/ssh/.golangci.yml b/vendor/charm.land/ssh/.golangci.yml new file mode 100644 index 0000000000..c3b50844d1 --- /dev/null +++ b/vendor/charm.land/ssh/.golangci.yml @@ -0,0 +1,68 @@ +version: "2" +run: + tests: false +linters: + enable: + - bodyclose + - exhaustive + - goconst + - godot + - gomoddirectives + - goprintffuncname + - gosec + - misspell + - nakedret + - nestif + - nilerr + - noctx + - nolintlint + - prealloc + - revive + - rowserrcheck + - sqlclosecheck + - tparallel + - unconvert + - unparam + - whitespace + - wrapcheck + exclusions: + rules: + - text: '(slog|log)\.\w+' + linters: + - noctx + - text: "var-naming" + linters: + - revive + # This package is a thin wrapper around x/crypto/ssh and the stdlib. + # Errors are passed through as-is by design; wrapping them adds no + # value to the caller. + - text: "error returned from (external package|interface method)" + linters: + - wrapcheck + # Port numbers, terminal dimensions, and exit codes are all bounded + # by the SSH protocol and cannot overflow in practice. + - text: "G115: integer overflow conversion" + linters: + - gosec + # net.Listen in this package is for server-side sockets, not HTTP + # client requests. + - text: "net.Listen must not be called" + linters: + - noctx + # The accept loop backoff pattern matches net/http and is not + # improved by further extraction. + - text: "`if e != nil` has complex nested blocks" + linters: + - nestif + generated: lax + presets: + - common-false-positives +issues: + max-issues-per-linter: 0 + max-same-issues: 0 +formatters: + enable: + - gofumpt + - goimports + exclusions: + generated: lax diff --git a/vendor/charm.land/ssh/.goreleaser.yml b/vendor/charm.land/ssh/.goreleaser.yml new file mode 100644 index 0000000000..305cb122bc --- /dev/null +++ b/vendor/charm.land/ssh/.goreleaser.yml @@ -0,0 +1,4 @@ +includes: + - from_url: + url: charmbracelet/meta/main/goreleaser-lib.yaml +# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json diff --git a/vendor/charm.land/ssh/LICENSE b/vendor/charm.land/ssh/LICENSE new file mode 100644 index 0000000000..4a03f02a28 --- /dev/null +++ b/vendor/charm.land/ssh/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2016 Glider Labs. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Glider Labs nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/charm.land/ssh/README.md b/vendor/charm.land/ssh/README.md new file mode 100644 index 0000000000..6706a14525 --- /dev/null +++ b/vendor/charm.land/ssh/README.md @@ -0,0 +1,72 @@ +# ssh + +
+ +An SSH server library for Go. `ssh` wraps the lower-level +[x/crypto/ssh](https://pkg.go.dev/golang.org/x/crypto/ssh) package with a +higher-level API that feels like [net/http](https://pkg.go.dev/net/http): + +```go +package main + +import ( + "io" + "log" + + "charm.land/ssh" +) + +func main() { + ssh.Handle(func(s ssh.Session) { + io.WriteString(s, "Hello world\n") + }) + + log.Fatal(ssh.ListenAndServe(":2222", nil)) +} +``` + +## Features + +* Familiar, `net/http`-inspired API +* Handlers for sessions, channels, port forwarding, and more +* Public key, password, and keyboard-interactive auth +* PTY support across platforms +* Used in production by [Soft Serve](https://github.com/charmbracelet/soft-serve) + and [Wish](https://github.com/charmbracelet/wish) + +## Examples + +A bunch of great examples are in the [`_examples`](_examples) directory. + +## Usage + +[See the GoDoc reference.](https://pkg.go.dev/charm.land/ssh) + +## Feedback + +We’d love to hear your thoughts on this project. Feel free to drop us a note! + +* [Twitter](https://twitter.com/charmcli) +* [The Fediverse](https://mastodon.social/@charmcli) +* [Discord](https://charm.land/chat) + +## Acknowledgements + +This package was originally forked from [gliderlabs/ssh](https://github.com/gliderlabs/ssh) + +## License + +[MIT](https://github.com/charmbracelet/ssh/raw/master/LICENSE) + +*** + +Part of [Charm](https://charm.land). + +
+
+Charm热爱开源 • Charm loves open source
diff --git a/vendor/charm.land/ssh/Taskfile.yaml b/vendor/charm.land/ssh/Taskfile.yaml
new file mode 100644
index 0000000000..71bba9b6b4
--- /dev/null
+++ b/vendor/charm.land/ssh/Taskfile.yaml
@@ -0,0 +1,53 @@
+# https://taskfile.dev
+
+version: "3"
+
+tasks:
+ fmt:
+ desc: Run gofumpt and goimports
+ cmds:
+ - golangci-lint fmt
+
+ lint:
+ desc: Run base linters
+ cmds:
+ - golangci-lint run
+
+ lint:fix:
+ desc: Run linters and apply automatic fixes
+ cmds:
+ - golangci-lint run --fix
+
+ test:
+ desc: Run tests
+ cmds:
+ - go test -race ./... {{.CLI_ARGS}}
+
+ tidy:
+ desc: Run go mod tidy
+ cmds:
+ - go mod tidy
+
+ release:
+ desc: Create and push a new tag following semver
+ vars:
+ NEXT:
+ sh: svu next --always || go run github.com/caarlos0/svu/v3@latest next --always
+ prompt: "This will release {{.NEXT}}. Continue?"
+ preconditions:
+ - sh: '[ $(git symbolic-ref --short HEAD) = "master" ]'
+ msg: Not on master branch
+ - sh: "[ $(git status --porcelain=2 | wc -l) = 0 ]"
+ msg: "Git is dirty"
+ - sh: 'gh run list --workflow build.yml --commit $(git rev-parse HEAD) --status success --json conclusion -q ".[0].conclusion" | grep -q success'
+ msg: "Test build for this commit failed or not present"
+ cmds:
+ - task: fetch-tags
+ - git commit --allow-empty -m "{{.NEXT}}"
+ - git tag --annotate --sign -m "{{.NEXT}}" {{.NEXT}} {{.CLI_ARGS}}
+ - echo "Pushing {{.NEXT}}..."
+ - git push origin master --follow-tags
+
+ fetch-tags:
+ cmds:
+ - git fetch --tags
diff --git a/vendor/charm.land/ssh/agent.go b/vendor/charm.land/ssh/agent.go
new file mode 100644
index 0000000000..0ddf1ede86
--- /dev/null
+++ b/vendor/charm.land/ssh/agent.go
@@ -0,0 +1,90 @@
+package ssh
+
+import (
+ "io"
+ "net"
+ "os"
+ "path"
+ "sync"
+
+ gossh "golang.org/x/crypto/ssh"
+)
+
+const (
+ agentRequestType = "auth-agent-req@openssh.com"
+ agentChannelType = "auth-agent@openssh.com"
+
+ agentTempDir = "auth-agent"
+ agentListenFile = "listener.sock"
+)
+
+// contextKeyAgentRequest is an internal context key for storing if the
+// client requested agent forwarding.
+var contextKeyAgentRequest = &contextKey{"auth-agent-req"}
+
+// SetAgentRequested sets up the session context so that AgentRequested
+// returns true.
+func SetAgentRequested(ctx Context) {
+ ctx.SetValue(contextKeyAgentRequest, true)
+}
+
+// AgentRequested returns true if the client requested agent forwarding.
+func AgentRequested(sess Session) bool {
+ return sess.Context().Value(contextKeyAgentRequest) == true
+}
+
+// NewAgentListener sets up a temporary Unix socket that can be communicated
+// to the session environment and used for forwarding connections.
+func NewAgentListener() (net.Listener, error) {
+ dir, err := os.MkdirTemp("", agentTempDir)
+ if err != nil {
+ return nil, err
+ }
+ l, err := net.Listen("unix", path.Join(dir, agentListenFile))
+ if err != nil {
+ return nil, err
+ }
+ return l, nil
+}
+
+// ForwardAgentConnections takes connections from a listener to proxy into the
+// session on the OpenSSH channel for agent connections. It blocks and services
+// connections until the listener stop accepting.
+func ForwardAgentConnections(l net.Listener, s Session) {
+ sshConn := s.Context().Value(ContextKeyConn).(gossh.Conn)
+ for {
+ conn, err := l.Accept()
+ if err != nil {
+ return
+ }
+ go func(conn net.Conn) {
+ defer recoverAndLog("panic forwarding agent connection", nil, nil)
+ defer func() { _ = conn.Close() }()
+ channel, reqs, err := sshConn.OpenChannel(agentChannelType, nil)
+ if err != nil {
+ return
+ }
+ defer func() { _ = channel.Close() }()
+ go gossh.DiscardRequests(reqs)
+ var wg sync.WaitGroup
+ wg.Add(2)
+ go func() {
+ // Done is deferred ahead of the recover so that it runs even
+ // if the copy panics. Otherwise the recover would turn a crash
+ // into a permanently blocked Wait below, leaking this
+ // goroutine along with the socket and the channel.
+ defer wg.Done()
+ defer recoverAndLog("panic proxying agent connection", nil, nil)
+ _, _ = io.Copy(conn, channel)
+ _ = conn.(*net.UnixConn).CloseWrite()
+ }()
+ go func() {
+ defer wg.Done()
+ defer recoverAndLog("panic proxying agent connection", nil, nil)
+ _, _ = io.Copy(channel, conn)
+ _ = channel.CloseWrite()
+ }()
+ wg.Wait()
+ }(conn)
+ }
+}
diff --git a/vendor/charm.land/ssh/conn.go b/vendor/charm.land/ssh/conn.go
new file mode 100644
index 0000000000..d8dca8deef
--- /dev/null
+++ b/vendor/charm.land/ssh/conn.go
@@ -0,0 +1,63 @@
+package ssh
+
+import (
+ "context"
+ "net"
+ "time"
+)
+
+type serverConn struct {
+ net.Conn
+
+ idleTimeout time.Duration
+ handshakeDeadline time.Time
+ maxDeadline time.Time
+ closeCanceler context.CancelFunc
+}
+
+func (c *serverConn) Write(p []byte) (n int, err error) {
+ if c.idleTimeout > 0 {
+ c.updateDeadline()
+ }
+ n, err = c.Conn.Write(p)
+ if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
+ c.closeCanceler()
+ }
+ return
+}
+
+func (c *serverConn) Read(b []byte) (n int, err error) {
+ if c.idleTimeout > 0 {
+ c.updateDeadline()
+ }
+ n, err = c.Conn.Read(b)
+ if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
+ c.closeCanceler()
+ }
+ return
+}
+
+func (c *serverConn) Close() (err error) {
+ err = c.Conn.Close()
+ if c.closeCanceler != nil {
+ c.closeCanceler()
+ }
+ return
+}
+
+func (c *serverConn) updateDeadline() {
+ deadline := c.maxDeadline
+
+ if !c.handshakeDeadline.IsZero() && (deadline.IsZero() || c.handshakeDeadline.Before(deadline)) {
+ deadline = c.handshakeDeadline
+ }
+
+ if c.idleTimeout > 0 {
+ idleDeadline := time.Now().Add(c.idleTimeout)
+ if deadline.IsZero() || idleDeadline.Before(deadline) {
+ deadline = idleDeadline
+ }
+ }
+
+ _ = c.SetDeadline(deadline)
+}
diff --git a/vendor/charm.land/ssh/context.go b/vendor/charm.land/ssh/context.go
new file mode 100644
index 0000000000..ab7f1e3dc0
--- /dev/null
+++ b/vendor/charm.land/ssh/context.go
@@ -0,0 +1,192 @@
+package ssh
+
+import (
+ "context"
+ "encoding/hex"
+ "net"
+ "sync"
+
+ gossh "golang.org/x/crypto/ssh"
+)
+
+// contextKey is a value for use with context.WithValue. It's used as
+// a pointer so it fits in an interface{} without allocation.
+type contextKey struct {
+ name string
+}
+
+var (
+ // ContextKeyUser is a context key for use with Contexts in this package.
+ // The associated value will be of type string.
+ ContextKeyUser = &contextKey{"user"}
+
+ // ContextKeySessionID is a context key for use with Contexts in this package.
+ // The associated value will be of type string.
+ ContextKeySessionID = &contextKey{"session-id"}
+
+ // ContextKeyPermissions is a context key for use with Contexts in this package.
+ // The associated value will be of type *Permissions.
+ ContextKeyPermissions = &contextKey{"permissions"}
+
+ // ContextKeyClientVersion is a context key for use with Contexts in this package.
+ // The associated value will be of type string.
+ ContextKeyClientVersion = &contextKey{"client-version"}
+
+ // ContextKeyServerVersion is a context key for use with Contexts in this package.
+ // The associated value will be of type string.
+ ContextKeyServerVersion = &contextKey{"server-version"}
+
+ // ContextKeyLocalAddr is a context key for use with Contexts in this package.
+ // The associated value will be of type net.Addr.
+ ContextKeyLocalAddr = &contextKey{"local-addr"}
+
+ // ContextKeyRemoteAddr is a context key for use with Contexts in this package.
+ // The associated value will be of type net.Addr.
+ ContextKeyRemoteAddr = &contextKey{"remote-addr"}
+
+ // ContextKeyServer is a context key for use with Contexts in this package.
+ // The associated value will be of type *Server.
+ ContextKeyServer = &contextKey{"ssh-server"}
+
+ // ContextKeyConn is a context key for use with Contexts in this package.
+ // The associated value will be of type gossh.ServerConn.
+ ContextKeyConn = &contextKey{"ssh-conn"}
+
+ // ContextKeyPublicKey is a context key for use with Contexts in this package.
+ // The associated value will be of type PublicKey.
+ ContextKeyPublicKey = &contextKey{"public-key"}
+
+ // ContextKeySession is a context key for use with Contexts in this package.
+ // The associated value will be of type Session.
+ ContextKeySession = &contextKey{"session"}
+)
+
+// Context is a package specific context interface. It exposes connection
+// metadata and allows new values to be easily written to it. It's used in
+// authentication handlers and callbacks, and its underlying context.Context is
+// exposed on Session in the session Handler. A connection-scoped lock is also
+// embedded in the context to make it easier to limit operations per-connection.
+type Context interface {
+ context.Context
+ sync.Locker
+
+ // User returns the username used when establishing the SSH connection.
+ User() string
+
+ // SessionID returns the session hash.
+ SessionID() string
+
+ // ClientVersion returns the version reported by the client.
+ ClientVersion() string
+
+ // ServerVersion returns the version reported by the server.
+ ServerVersion() string
+
+ // RemoteAddr returns the remote address for this connection.
+ RemoteAddr() net.Addr
+
+ // LocalAddr returns the local address for this connection.
+ LocalAddr() net.Addr
+
+ // Permissions returns the Permissions object used for this connection.
+ Permissions() *Permissions
+
+ // SetValue allows you to easily write new values into the underlying context.
+ SetValue(key, value interface{})
+}
+
+type sshContext struct {
+ context.Context
+ *sync.Mutex
+
+ values map[interface{}]interface{}
+ valuesMu sync.Mutex
+}
+
+var _ context.Context = &sshContext{}
+
+var _ sync.Locker = &sshContext{}
+
+func newContext(srv *Server) (*sshContext, context.CancelFunc) {
+ innerCtx, cancel := context.WithCancel(context.Background())
+ ctx := &sshContext{
+ Context: innerCtx,
+ Mutex: &sync.Mutex{},
+ values: make(map[interface{}]interface{}),
+ }
+ ctx.SetValue(ContextKeyServer, srv)
+ perms := &Permissions{&gossh.Permissions{}}
+ ctx.SetValue(ContextKeyPermissions, perms)
+ return ctx, cancel
+}
+
+func resetPermissions(ctx Context) {
+ ctx.Permissions().Permissions = &gossh.Permissions{}
+}
+
+// this is separate from newContext because we will get ConnMetadata
+// at different points so it needs to be applied separately.
+func applyConnMetadata(ctx Context, conn gossh.ConnMetadata) {
+ // The username is per-authentication-attempt and can change between
+ // attempts on the same connection, so it must be refreshed every time.
+ // The remaining values are connection-scoped and set only once.
+ ctx.SetValue(ContextKeyUser, conn.User())
+
+ if ctx.Value(ContextKeySessionID) != nil {
+ return
+ }
+ ctx.SetValue(ContextKeySessionID, hex.EncodeToString(conn.SessionID()))
+ ctx.SetValue(ContextKeyClientVersion, string(conn.ClientVersion()))
+ ctx.SetValue(ContextKeyServerVersion, string(conn.ServerVersion()))
+ ctx.SetValue(ContextKeyLocalAddr, conn.LocalAddr())
+ ctx.SetValue(ContextKeyRemoteAddr, conn.RemoteAddr())
+}
+
+func (ctx *sshContext) Value(key interface{}) interface{} {
+ ctx.valuesMu.Lock()
+ defer ctx.valuesMu.Unlock()
+ if v, ok := ctx.values[key]; ok {
+ return v
+ }
+ return ctx.Context.Value(key)
+}
+
+func (ctx *sshContext) SetValue(key, value interface{}) {
+ ctx.valuesMu.Lock()
+ defer ctx.valuesMu.Unlock()
+ ctx.values[key] = value
+}
+
+func (ctx *sshContext) User() string {
+ return ctx.Value(ContextKeyUser).(string)
+}
+
+func (ctx *sshContext) SessionID() string {
+ return ctx.Value(ContextKeySessionID).(string)
+}
+
+func (ctx *sshContext) ClientVersion() string {
+ return ctx.Value(ContextKeyClientVersion).(string)
+}
+
+func (ctx *sshContext) ServerVersion() string {
+ return ctx.Value(ContextKeyServerVersion).(string)
+}
+
+func (ctx *sshContext) RemoteAddr() net.Addr {
+ if addr, ok := ctx.Value(ContextKeyRemoteAddr).(net.Addr); ok {
+ return addr
+ }
+ return nil
+}
+
+func (ctx *sshContext) LocalAddr() net.Addr {
+ if addr, ok := ctx.Value(ContextKeyLocalAddr).(net.Addr); ok {
+ return addr
+ }
+ return nil
+}
+
+func (ctx *sshContext) Permissions() *Permissions {
+ return ctx.Value(ContextKeyPermissions).(*Permissions)
+}
diff --git a/vendor/charm.land/ssh/doc.go b/vendor/charm.land/ssh/doc.go
new file mode 100644
index 0000000000..d139191768
--- /dev/null
+++ b/vendor/charm.land/ssh/doc.go
@@ -0,0 +1,45 @@
+/*
+Package ssh wraps the crypto/ssh package with a higher-level API for building
+SSH servers. The goal of the API was to make it as simple as using net/http, so
+the API is very similar.
+
+You should be able to build any SSH server using only this package, which wraps
+relevant types and some functions from crypto/ssh. However, you still need to
+use crypto/ssh for building SSH clients.
+
+ListenAndServe starts an SSH server with a given address, handler, and options. The
+handler is usually nil, which means to use DefaultHandler. Handle sets DefaultHandler:
+
+ ssh.Handle(func(s ssh.Session) {
+ io.WriteString(s, "Hello world\n")
+ })
+
+ log.Fatal(ssh.ListenAndServe(":2222", nil))
+
+If you don't specify a host key, it will generate one every time. This is convenient
+except you'll have to deal with clients being confused that the host key is different.
+It's a better idea to generate or point to an existing key on your system:
+
+ log.Fatal(ssh.ListenAndServe(":2222", nil, ssh.HostKeyFile("/Users/progrium/.ssh/id_rsa")))
+
+Although all options have functional option helpers, another way to control the
+server's behavior is by creating a custom Server:
+
+ s := &ssh.Server{
+ Addr: ":2222",
+ Handler: sessionHandler,
+ PublicKeyHandler: authHandler,
+ }
+ s.AddHostKey(hostKeySigner)
+
+ log.Fatal(s.ListenAndServe())
+
+This package automatically handles basic SSH requests like setting environment
+variables, requesting PTY, and changing window size. These requests are
+processed, responded to, and any relevant state is updated. This state is then
+exposed to you via the Session interface.
+
+The one big feature missing from the Session abstraction is signals. This was
+started, but not completed. Pull Requests welcome!
+*/
+package ssh
diff --git a/vendor/charm.land/ssh/options.go b/vendor/charm.land/ssh/options.go
new file mode 100644
index 0000000000..e87ec2416f
--- /dev/null
+++ b/vendor/charm.land/ssh/options.go
@@ -0,0 +1,122 @@
+package ssh
+
+import (
+ "os"
+
+ gossh "golang.org/x/crypto/ssh"
+)
+
+// PasswordAuth returns a functional option that sets PasswordHandler on the server.
+func PasswordAuth(fn PasswordHandler) Option {
+ return func(srv *Server) error {
+ srv.PasswordHandler = fn
+ return nil
+ }
+}
+
+// PublicKeyAuth returns a functional option that sets PublicKeyHandler on the server.
+func PublicKeyAuth(fn PublicKeyHandler) Option {
+ return func(srv *Server) error {
+ srv.PublicKeyHandler = fn
+ return nil
+ }
+}
+
+// HostKeyFile returns a functional option that adds HostSigners to the server
+// from a PEM file at filepath.
+func HostKeyFile(filepath string) Option {
+ return func(srv *Server) error {
+ pemBytes, err := os.ReadFile(filepath)
+ if err != nil {
+ return err
+ }
+
+ signer, err := gossh.ParsePrivateKey(pemBytes)
+ if err != nil {
+ return err
+ }
+
+ srv.AddHostKey(signer)
+
+ return nil
+ }
+}
+
+// KeyboardInteractiveAuth returns a functional option that sets the
+// keyboard-interactive authentication handler.
+func KeyboardInteractiveAuth(fn KeyboardInteractiveHandler) Option {
+ return func(srv *Server) error {
+ srv.KeyboardInteractiveHandler = fn
+ return nil
+ }
+}
+
+// HostKeyPEM returns a functional option that adds HostSigners to the server
+// from a PEM file as bytes.
+func HostKeyPEM(bytes []byte) Option {
+ return func(srv *Server) error {
+ signer, err := gossh.ParsePrivateKey(bytes)
+ if err != nil {
+ return err
+ }
+
+ srv.AddHostKey(signer)
+
+ return nil
+ }
+}
+
+// NoPty returns a functional option that sets PtyCallback to return false,
+// denying PTY requests.
+func NoPty() Option {
+ return func(srv *Server) error {
+ srv.PtyCallback = func(Context, Pty) bool {
+ return false
+ }
+ return nil
+ }
+}
+
+// WrapConn returns a functional option that sets ConnCallback on the server.
+func WrapConn(fn ConnCallback) Option {
+ return func(srv *Server) error {
+ srv.ConnCallback = fn
+ return nil
+ }
+}
+
+var contextKeyEmulatePty = &contextKey{"emulate-pty"}
+
+func emulatePtyHandler(ctx Context, _ Session, _ Pty) (func() error, error) {
+ ctx.SetValue(contextKeyEmulatePty, true)
+ return func() error { return nil }, nil
+}
+
+// EmulatePty returns a functional option that fakes a PTY. It uses PtyWriter
+// underneath.
+func EmulatePty() Option {
+ return func(s *Server) error {
+ s.PtyHandler = emulatePtyHandler
+ return nil
+ }
+}
+
+// AllocatePty returns a functional option that allocates a PTY. Implementers
+// who wish to use an actual PTY should use this along with the platform
+// specific PTY implementation defined in pty_*.go.
+func AllocatePty() Option {
+ return func(s *Server) error {
+ s.PtyHandler = func(_ Context, s Session, pty Pty) (func() error, error) {
+ return s.(*session).ptyAllocate(pty.Term, pty.Window, pty.Modes)
+ }
+ return nil
+ }
+}
+
+// EnableProxyProtocol returns a functional option that sets EnableProxyProtocol on the server.
+func EnableProxyProtocol() Option {
+ return func(srv *Server) error {
+ srv.EnableProxyProtocol = true
+ return nil
+ }
+}
diff --git a/vendor/charm.land/ssh/pty.go b/vendor/charm.land/ssh/pty.go
new file mode 100644
index 0000000000..f809162afb
--- /dev/null
+++ b/vendor/charm.land/ssh/pty.go
@@ -0,0 +1,96 @@
+package ssh
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "os/exec"
+)
+
+// ErrUnsupported is returned when the platform does not support PTY.
+var ErrUnsupported = errors.New("pty unsupported")
+
+// NewPtyWriter creates a writer that handles when the session has a active
+// PTY, replacing the \n with \r\n.
+func NewPtyWriter(w io.Writer) io.Writer {
+ return ptyWriter{
+ w: w,
+ }
+}
+
+var _ io.Writer = ptyWriter{}
+
+type ptyWriter struct {
+ w io.Writer
+}
+
+func (w ptyWriter) Write(p []byte) (int, error) {
+ m := len(p)
+ // normalize \n to \r\n when pty is accepted.
+ // this is a hardcoded shortcut since we don't support terminal modes.
+ p = bytes.ReplaceAll(p, []byte{'\n'}, []byte{'\r', '\n'})
+ p = bytes.ReplaceAll(p, []byte{'\r', '\r', '\n'}, []byte{'\r', '\n'})
+ n, err := w.w.Write(p)
+ if n > m {
+ n = m
+ }
+ return n, err
+}
+
+// NewPtyReadWriter return an io.ReadWriter that delegates the read to the
+// given io.ReadWriter, and the writes to a ptyWriter.
+func NewPtyReadWriter(rw io.ReadWriter) io.ReadWriter {
+ return readWriterDelegate{
+ w: NewPtyWriter(rw),
+ r: rw,
+ }
+}
+
+var _ io.ReadWriter = readWriterDelegate{}
+
+type readWriterDelegate struct {
+ w io.Writer
+ r io.Reader
+}
+
+func (rw readWriterDelegate) Read(p []byte) (n int, err error) {
+ return rw.r.Read(p)
+}
+
+func (rw readWriterDelegate) Write(p []byte) (n int, err error) {
+ return rw.w.Write(p)
+}
+
+// PtyStartOption configures how Pty.Start runs a command.
+type PtyStartOption func(*ptyStartConfig)
+
+type ptyStartConfig struct {
+ jobControl bool
+}
+
+// WithJobControl enables job control when starting the command: on Unix,
+// the command is put in its own session with the PTY slave as its
+// controlling terminal, so interactive shells get working signal handling
+// (e.g. ^C, ^Z).
+//
+// Leave it disabled (the default) when running multiple commands
+// sequentially against the same PTY, or when managing the command's process
+// attributes yourself via exec.Cmd.SysProcAttr.
+func WithJobControl() PtyStartOption {
+ return func(c *ptyStartConfig) {
+ c.jobControl = true
+ }
+}
+
+// Start starts a *exec.Cmd attached to the Session. If a PTY is allocated,
+// it will use that for I/O.
+// On Windows, the process execution lifecycle is not managed by Go and has to
+// be managed manually. This means that c.Wait() won't work.
+// See https://github.com/charmbracelet/x/blob/main/exp/term/conpty/conpty_windows.go
+func (p *Pty) Start(c *exec.Cmd, opts ...PtyStartOption) error {
+ cfg := ptyStartConfig{}
+ for _, opt := range opts {
+ opt(&cfg)
+ }
+ return p.start(c, cfg)
+}
diff --git a/vendor/charm.land/ssh/pty_other.go b/vendor/charm.land/ssh/pty_other.go
new file mode 100644
index 0000000000..79c7fa1de5
--- /dev/null
+++ b/vendor/charm.land/ssh/pty_other.go
@@ -0,0 +1,44 @@
+//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris && !windows
+// +build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris,!windows
+
+package ssh
+
+import (
+ "os/exec"
+
+ "golang.org/x/crypto/ssh"
+)
+
+type impl struct{}
+
+func (i *impl) IsZero() bool {
+ return true
+}
+
+func (i *impl) Name() string {
+ return ""
+}
+
+func (i *impl) Read(p []byte) (n int, err error) {
+ return 0, ErrUnsupported
+}
+
+func (i *impl) Write(p []byte) (n int, err error) {
+ return 0, ErrUnsupported
+}
+
+func (i *impl) Resize(w int, h int) error {
+ return ErrUnsupported
+}
+
+func (i *impl) Close() error {
+ return nil
+}
+
+func (*impl) start(*exec.Cmd, ptyStartConfig) error {
+ return ErrUnsupported
+}
+
+func newPty(Context, string, Window, ssh.TerminalModes) (impl, error) {
+ return impl{}, ErrUnsupported
+}
diff --git a/vendor/charm.land/ssh/pty_unix.go b/vendor/charm.land/ssh/pty_unix.go
new file mode 100644
index 0000000000..4186b6a58c
--- /dev/null
+++ b/vendor/charm.land/ssh/pty_unix.go
@@ -0,0 +1,225 @@
+//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
+// +build darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package ssh
+
+import (
+ "os"
+ "os/exec"
+ "syscall"
+
+ "github.com/charmbracelet/x/termios"
+ "github.com/creack/pty"
+ "golang.org/x/crypto/ssh"
+ "golang.org/x/sys/unix"
+)
+
+type impl struct {
+ // Master is the master PTY file descriptor.
+ Master *os.File
+
+ // Slave is the slave PTY file descriptor.
+ Slave *os.File
+}
+
+func (i *impl) IsZero() bool {
+ return i.Master == nil && i.Slave == nil
+}
+
+// Name returns the name of the slave PTY.
+func (i *impl) Name() string {
+ return i.Slave.Name()
+}
+
+// Read implements ptyInterface.
+func (i *impl) Read(p []byte) (n int, err error) {
+ return i.Master.Read(p)
+}
+
+// Write implements ptyInterface.
+func (i *impl) Write(p []byte) (n int, err error) {
+ return i.Master.Write(p)
+}
+
+func (i *impl) Close() error {
+ if err := i.Master.Close(); err != nil {
+ return err
+ }
+ return i.Slave.Close()
+}
+
+func (i *impl) Resize(w int, h int) (rErr error) {
+ conn, err := i.Master.SyscallConn()
+ if err != nil {
+ return err
+ }
+
+ return conn.Control(func(fd uintptr) {
+ rErr = termios.SetWinsize(int(fd), &unix.Winsize{
+ Row: uint16(h),
+ Col: uint16(w),
+ })
+ })
+}
+
+func (i *impl) start(c *exec.Cmd, cfg ptyStartConfig) error {
+ c.Stdin, c.Stdout, c.Stderr = i.Slave, i.Slave, i.Slave
+ if cfg.jobControl {
+ if c.SysProcAttr == nil {
+ c.SysProcAttr = &syscall.SysProcAttr{}
+ }
+ c.SysProcAttr.Setsid = true
+ c.SysProcAttr.Setctty = true
+ }
+ return c.Start()
+}
+
+func newPty(_ Context, _ string, win Window, modes ssh.TerminalModes) (_ impl, rErr error) {
+ ptm, pts, err := pty.Open()
+ if err != nil {
+ return impl{}, err
+ }
+
+ conn, err := ptm.SyscallConn()
+ if err != nil {
+ return impl{}, err
+ }
+
+ if err := conn.Control(func(fd uintptr) {
+ rErr = applyTerminalModesToFd(fd, win.Width, win.Height, modes)
+ }); err != nil {
+ return impl{}, err
+ }
+
+ return impl{Master: ptm, Slave: pts}, rErr
+}
+
+func applyTerminalModesToFd(fd uintptr, width int, height int, modes ssh.TerminalModes) error {
+ var ispeed, ospeed uint32
+ ccs := map[termios.CC]uint8{}
+ iflag := map[termios.I]bool{}
+ oflag := map[termios.O]bool{}
+ cflag := map[termios.C]bool{}
+ lflag := map[termios.L]bool{}
+
+ for op, value := range modes {
+ switch op {
+ case ssh.TTY_OP_ISPEED:
+ ispeed = value
+ case ssh.TTY_OP_OSPEED:
+ ospeed = value
+ default:
+ cc, ok := sshToCc[op]
+ if ok {
+ ccs[cc] = uint8(value)
+ continue
+ }
+ i, ok := sshToIflag[op]
+ if ok {
+ iflag[i] = value > 0
+ continue
+ }
+ o, ok := sshToOflag[op]
+ if ok {
+ oflag[o] = value > 0
+ continue
+ }
+
+ c, ok := sshToCflag[op]
+ if ok {
+ cflag[c] = value > 0
+ continue
+ }
+ l, ok := sshToLflag[op]
+ if ok {
+ lflag[l] = value > 0
+ continue
+ }
+ }
+ }
+ if err := termios.SetTermios(
+ int(fd),
+ ispeed,
+ ospeed,
+ ccs,
+ iflag,
+ oflag,
+ cflag,
+ lflag,
+ ); err != nil {
+ return err
+ }
+ return termios.SetWinsize(int(fd), &unix.Winsize{
+ Row: uint16(height),
+ Col: uint16(width),
+ })
+}
+
+var sshToCc = map[uint8]termios.CC{
+ ssh.VINTR: termios.INTR,
+ ssh.VQUIT: termios.QUIT,
+ ssh.VERASE: termios.ERASE,
+ ssh.VKILL: termios.KILL,
+ ssh.VEOF: termios.EOF,
+ ssh.VEOL: termios.EOL,
+ ssh.VEOL2: termios.EOL2,
+ ssh.VSTART: termios.START,
+ ssh.VSTOP: termios.STOP,
+ ssh.VSUSP: termios.SUSP,
+ ssh.VWERASE: termios.WERASE,
+ ssh.VREPRINT: termios.RPRNT,
+ ssh.VLNEXT: termios.LNEXT,
+ ssh.VDISCARD: termios.DISCARD,
+ ssh.VSTATUS: termios.STATUS,
+ ssh.VSWTCH: termios.SWTCH,
+ ssh.VFLUSH: termios.FLUSH,
+ ssh.VDSUSP: termios.DSUSP,
+}
+
+var sshToIflag = map[uint8]termios.I{
+ ssh.IGNPAR: termios.IGNPAR,
+ ssh.PARMRK: termios.PARMRK,
+ ssh.INPCK: termios.INPCK,
+ ssh.ISTRIP: termios.ISTRIP,
+ ssh.INLCR: termios.INLCR,
+ ssh.IGNCR: termios.IGNCR,
+ ssh.ICRNL: termios.ICRNL,
+ ssh.IUCLC: termios.IUCLC,
+ ssh.IXON: termios.IXON,
+ ssh.IXANY: termios.IXANY,
+ ssh.IXOFF: termios.IXOFF,
+ ssh.IMAXBEL: termios.IMAXBEL,
+}
+
+var sshToOflag = map[uint8]termios.O{
+ ssh.OPOST: termios.OPOST,
+ ssh.OLCUC: termios.OLCUC,
+ ssh.ONLCR: termios.ONLCR,
+ ssh.OCRNL: termios.OCRNL,
+ ssh.ONOCR: termios.ONOCR,
+ ssh.ONLRET: termios.ONLRET,
+}
+
+var sshToCflag = map[uint8]termios.C{
+ ssh.CS7: termios.CS7,
+ ssh.CS8: termios.CS8,
+ ssh.PARENB: termios.PARENB,
+ ssh.PARODD: termios.PARODD,
+}
+
+var sshToLflag = map[uint8]termios.L{
+ ssh.IUTF8: termios.IUTF8,
+ ssh.ISIG: termios.ISIG,
+ ssh.ICANON: termios.ICANON,
+ ssh.ECHO: termios.ECHO,
+ ssh.ECHOE: termios.ECHOE,
+ ssh.ECHOK: termios.ECHOK,
+ ssh.ECHONL: termios.ECHONL,
+ ssh.NOFLSH: termios.NOFLSH,
+ ssh.TOSTOP: termios.TOSTOP,
+ ssh.IEXTEN: termios.IEXTEN,
+ ssh.ECHOCTL: termios.ECHOCTL,
+ ssh.ECHOKE: termios.ECHOKE,
+ ssh.PENDIN: termios.PENDIN,
+ ssh.XCASE: termios.XCASE,
+}
diff --git a/vendor/charm.land/ssh/pty_windows.go b/vendor/charm.land/ssh/pty_windows.go
new file mode 100644
index 0000000000..627abce04d
--- /dev/null
+++ b/vendor/charm.land/ssh/pty_windows.go
@@ -0,0 +1,97 @@
+//go:build windows
+// +build windows
+
+package ssh
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "syscall"
+
+ "github.com/charmbracelet/x/conpty"
+ "golang.org/x/crypto/ssh"
+ "golang.org/x/sys/windows"
+)
+
+type impl struct {
+ Context
+ *conpty.ConPty
+}
+
+func (i *impl) IsZero() bool {
+ return i.ConPty == nil
+}
+
+func (i *impl) Name() string {
+ return "windows-pty"
+}
+
+func (i *impl) Read(p []byte) (n int, err error) {
+ return i.ConPty.Read(p)
+}
+
+func (i *impl) Write(p []byte) (n int, err error) {
+ return i.ConPty.Write(p)
+}
+
+func (i *impl) Resize(w int, h int) error {
+ return i.ConPty.Resize(w, h)
+}
+
+func (i *impl) Close() error {
+ return i.ConPty.Close()
+}
+
+func (i *impl) start(c *exec.Cmd, _ ptyStartConfig) error {
+ pid, process, err := i.Spawn(c.Path, c.Args, &syscall.ProcAttr{
+ Dir: c.Dir,
+ Env: c.Env,
+ Sys: c.SysProcAttr,
+ })
+ if err != nil {
+ return err
+ }
+
+ c.Process, err = os.FindProcess(pid)
+ if err != nil {
+ // If we can't find the process via os.FindProcess, terminate the
+ // process as that's what we rely on for all further operations on the
+ // object.
+ if tErr := windows.TerminateProcess(windows.Handle(process), 1); tErr != nil {
+ return fmt.Errorf("failed to terminate process after process not found: %w", tErr)
+ }
+ return fmt.Errorf("failed to find process after starting: %w", err)
+ }
+
+ type result struct {
+ *os.ProcessState
+ error
+ }
+ donec := make(chan result, 1)
+ go func() {
+ state, err := c.Process.Wait()
+ donec <- result{state, err}
+ }()
+ go func() {
+ defer windows.CloseHandle(windows.Handle(process)) //nolint:errcheck
+ select {
+ case <-i.Done():
+ c.Err = windows.TerminateProcess(windows.Handle(process), 1)
+ case r := <-donec:
+ c.ProcessState = r.ProcessState
+ c.Err = r.error
+ }
+ }()
+
+ return nil
+}
+
+func newPty(ctx Context, _ string, win Window, _ ssh.TerminalModes) (impl, error) {
+ c, err := conpty.New(win.Width, win.Height, 0)
+ if err != nil {
+ return impl{}, err
+ }
+
+ return impl{ctx, c}, nil
+}
diff --git a/vendor/charm.land/ssh/server.go b/vendor/charm.land/ssh/server.go
new file mode 100644
index 0000000000..98d5e8512c
--- /dev/null
+++ b/vendor/charm.land/ssh/server.go
@@ -0,0 +1,642 @@
+package ssh
+
+import (
+ "context"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "log/slog"
+ "net"
+ "runtime/debug"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/pires/go-proxyproto"
+ gossh "golang.org/x/crypto/ssh"
+)
+
+// ErrServerClosed is returned by the Server's Serve, ListenAndServe,
+// and ListenAndServeTLS methods after a call to Shutdown or Close.
+var ErrServerClosed = errors.New("ssh: Server closed")
+
+// SubsystemHandler is a handler for a given SSH subsystem.
+type SubsystemHandler func(s Session)
+
+// DefaultSubsystemHandlers is the default set of subsystem handlers.
+var DefaultSubsystemHandlers = map[string]SubsystemHandler{}
+
+// RequestHandler is a callback for custom global SSH requests.
+type RequestHandler func(ctx Context, srv *Server, req *gossh.Request) (ok bool, payload []byte)
+
+// DefaultRequestHandlers is the default set of request handlers.
+var DefaultRequestHandlers = map[string]RequestHandler{}
+
+// ChannelHandler is a callback for custom channel types.
+type ChannelHandler func(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context)
+
+// DefaultChannelHandlers is the default set of channel handlers.
+var DefaultChannelHandlers = map[string]ChannelHandler{
+ "session": DefaultSessionHandler,
+}
+
+var permissionsPublicKeyExt = "gliderlabs/ssh.PublicKey"
+
+// ErrPermissionDenied is returned when authentication fails.
+var ErrPermissionDenied = errors.New("permission denied")
+
+func ensureNoPKInPermissions(ctx Context) error {
+ if _, ok := ctx.Permissions().Extensions[permissionsPublicKeyExt]; ok {
+ return errors.New("misconfigured server: public key incorrectly set")
+ }
+
+ return nil
+}
+
+// Server defines parameters for running an SSH server. The zero value for
+// Server is a valid configuration. When both PasswordHandler and
+// PublicKeyHandler are nil, no client authentication is performed.
+type Server struct {
+ Addr string // TCP address to listen on, ":22" if empty
+ Handler Handler // handler to invoke, ssh.DefaultHandler if nil
+ HostSigners []Signer // private keys for the host key, must have at least one
+ Version string // server version to be sent before the initial handshake
+ Banner string // server banner
+
+ BannerHandler BannerHandler // server banner handler, overrides Banner
+ KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler
+ PasswordHandler PasswordHandler // password authentication handler
+ PublicKeyHandler PublicKeyHandler // public key authentication handler
+ PtyCallback PtyCallback // callback for allocating and allowing PTY sessions, ssh.EmulatePtyCallback if nil
+ PtyHandler PtyHandler // pty allocation handler, ssh.emulatePtyHandler if nil
+ ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
+ LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
+ ReversePortForwardingCallback ReversePortForwardingCallback // callback for allowing reverse port forwarding, denies all if nil
+ ServerConfigCallback ServerConfigCallback // callback for configuring detailed SSH options
+ SessionRequestCallback SessionRequestCallback // callback for allowing or denying SSH sessions
+
+ ConnectionFailedCallback ConnectionFailedCallback // callback to report connection failures
+ ConnectionCloseCallback ConnectionCloseCallback // callback to report connection close
+
+ HandshakeTimeout time.Duration // connection timeout until successful handshake, none if empty
+ IdleTimeout time.Duration // connection timeout when no activity, none if empty
+ MaxTimeout time.Duration // absolute connection timeout, none if empty
+
+ EnableProxyProtocol bool // Enable support for HA Proxy's and NGinx's PROXY protocol
+
+ // ChannelHandlers allow overriding the built-in session handlers or provide
+ // extensions to the protocol, such as tcpip forwarding. By default only the
+ // "session" handler is enabled.
+ ChannelHandlers map[string]ChannelHandler
+
+ // RequestHandlers allow overriding the server-level request handlers or
+ // provide extensions to the protocol, such as tcpip forwarding. By default
+ // no handlers are enabled.
+ RequestHandlers map[string]RequestHandler
+
+ // SubsystemHandlers are handlers which are similar to the usual SSH command
+ // handlers, but handle named subsystems.
+ SubsystemHandlers map[string]SubsystemHandler
+
+ listenerWg sync.WaitGroup
+ mu sync.RWMutex
+ started atomic.Bool
+ listeners map[net.Listener]struct{}
+ conns map[*gossh.ServerConn]struct{}
+ connWg sync.WaitGroup
+ doneChan chan struct{}
+}
+
+func (srv *Server) ensureHostSigner() error {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ if len(srv.HostSigners) == 0 {
+ signer, err := generateSigner()
+ if err != nil {
+ return err
+ }
+ srv.HostSigners = append(srv.HostSigners, signer)
+ }
+ return nil
+}
+
+func (srv *Server) ensureHandlers() {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ if srv.RequestHandlers == nil {
+ srv.RequestHandlers = map[string]RequestHandler{}
+ for k, v := range DefaultRequestHandlers {
+ srv.RequestHandlers[k] = v
+ }
+ }
+ if srv.ChannelHandlers == nil {
+ srv.ChannelHandlers = map[string]ChannelHandler{}
+ for k, v := range DefaultChannelHandlers {
+ srv.ChannelHandlers[k] = v
+ }
+ }
+ if srv.SubsystemHandlers == nil {
+ srv.SubsystemHandlers = map[string]SubsystemHandler{}
+ for k, v := range DefaultSubsystemHandlers {
+ srv.SubsystemHandlers[k] = v
+ }
+ }
+}
+
+func (srv *Server) config(ctx Context) *gossh.ServerConfig {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ var config *gossh.ServerConfig
+ if srv.ServerConfigCallback == nil {
+ config = &gossh.ServerConfig{}
+ } else {
+ config = srv.ServerConfigCallback(ctx)
+ }
+ for _, signer := range srv.HostSigners {
+ config.AddHostKey(signer)
+ }
+ if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil && srv.KeyboardInteractiveHandler == nil &&
+ config.PasswordCallback == nil && config.PublicKeyCallback == nil && config.KeyboardInteractiveCallback == nil {
+ config.NoClientAuth = true
+ }
+ if srv.PtyHandler == nil {
+ srv.PtyHandler = emulatePtyHandler
+ }
+ if srv.Version != "" {
+ config.ServerVersion = "SSH-2.0-" + srv.Version
+ }
+ if srv.Banner != "" {
+ config.BannerCallback = func(_ gossh.ConnMetadata) string {
+ return srv.Banner
+ }
+ }
+ if srv.BannerHandler != nil {
+ config.BannerCallback = func(conn gossh.ConnMetadata) string {
+ applyConnMetadata(ctx, conn)
+ return srv.BannerHandler(ctx)
+ }
+ }
+ if srv.PasswordHandler != nil {
+ config.PasswordCallback = func(conn gossh.ConnMetadata, password []byte) (*gossh.Permissions, error) {
+ resetPermissions(ctx)
+ applyConnMetadata(ctx, conn)
+ err := ensureNoPKInPermissions(ctx)
+ if err != nil {
+ return ctx.Permissions().Permissions, err
+ }
+ ok := srv.PasswordHandler(ctx, string(password))
+ if !ok {
+ return ctx.Permissions().Permissions, ErrPermissionDenied
+ }
+ return ctx.Permissions().Permissions, nil
+ }
+ }
+ if srv.PublicKeyHandler != nil {
+ config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) {
+ resetPermissions(ctx)
+ applyConnMetadata(ctx, conn)
+ err := ensureNoPKInPermissions(ctx)
+ if err != nil {
+ return ctx.Permissions().Permissions, err
+ }
+ ok := srv.PublicKeyHandler(ctx, key)
+ if !ok {
+ return ctx.Permissions().Permissions, ErrPermissionDenied
+ }
+
+ pkStr := base64.StdEncoding.EncodeToString(key.Marshal())
+ if ctx.Permissions().Extensions == nil {
+ ctx.Permissions().Extensions = map[string]string{}
+ }
+ ctx.Permissions().Extensions[permissionsPublicKeyExt] = pkStr
+
+ return ctx.Permissions().Permissions, nil
+ }
+ }
+ if srv.KeyboardInteractiveHandler != nil {
+ config.KeyboardInteractiveCallback = func(conn gossh.ConnMetadata, challenger gossh.KeyboardInteractiveChallenge) (*gossh.Permissions, error) {
+ resetPermissions(ctx)
+ applyConnMetadata(ctx, conn)
+ ok := srv.KeyboardInteractiveHandler(ctx, challenger)
+ err := ensureNoPKInPermissions(ctx)
+ if err != nil {
+ return ctx.Permissions().Permissions, err
+ }
+ if !ok {
+ return ctx.Permissions().Permissions, ErrPermissionDenied
+ }
+ return ctx.Permissions().Permissions, nil
+ }
+ }
+ return config
+}
+
+// Handle sets the Handler for the server.
+func (srv *Server) Handle(fn Handler) {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ srv.Handler = fn
+}
+
+// Close immediately closes all active listeners and all active
+// connections.
+//
+// Close returns any error returned from closing the Server's
+// underlying Listener(s).
+func (srv *Server) Close() error {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ srv.closeDoneChanLocked()
+ err := srv.closeListenersLocked()
+ for c := range srv.conns {
+ _ = c.Close()
+ delete(srv.conns, c)
+ }
+ return err
+}
+
+// Shutdown gracefully shuts down the server without interrupting any
+// active connections. Shutdown works by first closing all open
+// listeners, and then waiting indefinitely for connections to close.
+// If the provided context expires before the shutdown is complete,
+// then the context's error is returned.
+func (srv *Server) Shutdown(ctx context.Context) error {
+ srv.mu.Lock()
+ lnerr := srv.closeListenersLocked()
+ srv.closeDoneChanLocked()
+ srv.mu.Unlock()
+
+ finished := make(chan struct{}, 1)
+ go func() {
+ srv.listenerWg.Wait()
+ srv.connWg.Wait()
+ finished <- struct{}{}
+ }()
+
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-finished:
+ return lnerr
+ }
+}
+
+// Serve accepts incoming connections on the Listener l, creating a new
+// connection goroutine for each. The connection goroutines read requests and then
+// calls srv.Handler to handle sessions.
+//
+// Serve always returns a non-nil error.
+func (srv *Server) Serve(l net.Listener) error {
+ if srv.EnableProxyProtocol {
+ _, ok := l.(*proxyproto.Listener)
+ if !ok {
+ l = &proxyproto.Listener{Listener: l}
+ }
+ }
+
+ srv.ensureHandlers()
+ defer func() { _ = l.Close() }()
+ if err := srv.ensureHostSigner(); err != nil {
+ return err
+ }
+ if srv.Handler == nil {
+ srv.Handler = DefaultHandler
+ }
+ var tempDelay time.Duration
+
+ srv.trackListener(l, true)
+ srv.started.Store(true)
+ defer srv.trackListener(l, false)
+ for {
+ conn, e := l.Accept()
+ if e != nil {
+ select {
+ case <-srv.getDoneChan():
+ return ErrServerClosed
+ default:
+ }
+ if ne, ok := e.(net.Error); ok && ne.Temporary() { //nolint:staticcheck // SA1019: same pattern as net/http
+ if tempDelay == 0 {
+ tempDelay = 5 * time.Millisecond
+ } else {
+ tempDelay *= 2
+ }
+ if limit := 1 * time.Second; tempDelay > limit {
+ tempDelay = limit
+ }
+ time.Sleep(tempDelay)
+ continue
+ }
+ return e
+ }
+ go srv.HandleConn(conn)
+ }
+}
+
+// HandleConn handles a new SSH connection.
+//
+// A panic while handling the connection is recovered and logged rather than
+// allowed to escape. Connections are served on their own goroutine, and Go has
+// no process-wide panic handler, so an unrecovered panic here would terminate
+// the whole server process along with every other connection. That includes
+// panics raised inside the SSH handshake, before authentication has happened,
+// which would otherwise let an unauthenticated client take the server down.
+func (srv *Server) HandleConn(newConn net.Conn) {
+ defer recoverAndLog("panic serving connection", newConn, func() {
+ // The deferred close inside handleConn did not get to run.
+ _ = newConn.Close()
+ })
+
+ srv.handleConn(newConn)
+}
+
+// recoverAndLog recovers a panic on the current goroutine, logs it with a
+// stack trace, and runs cleanup if one is given.
+//
+// This must be deferred directly on the goroutine to be protected: a recover
+// only sees panics unwinding its own stack, so every goroutine the server
+// starts needs its own call.
+func recoverAndLog(msg string, conn net.Conn, cleanup func()) {
+ r := recover()
+ if r == nil {
+ return
+ }
+ slog.Error("ssh: "+msg,
+ "remote", remoteAddrString(conn),
+ "panic", r,
+ "stack", string(debug.Stack()),
+ )
+ if cleanup != nil {
+ // Cleanup runs while already unwinding a panic, so a second panic
+ // raised here would escape this recover and kill the process, undoing
+ // the containment. Contain it separately.
+ defer func() {
+ if r := recover(); r != nil {
+ slog.Error("ssh: panic during "+msg+" cleanup", "panic", r)
+ }
+ }()
+ cleanup()
+ }
+}
+
+// remoteAddrString returns conn's remote address for logging, tolerating a nil
+// conn or address so that the panic handler cannot itself panic.
+func remoteAddrString(conn net.Conn) string {
+ if conn == nil {
+ return "unknown"
+ }
+ if addr := conn.RemoteAddr(); addr != nil {
+ return addr.String()
+ }
+ return "unknown"
+}
+
+func (srv *Server) handleConn(newConn net.Conn) {
+ ctx, cancel := newContext(srv)
+ if srv.ConnCallback != nil {
+ cbConn := srv.ConnCallback(ctx, newConn)
+ if cbConn == nil {
+ _ = newConn.Close()
+ return
+ }
+ newConn = cbConn
+ }
+ conn := &serverConn{
+ Conn: newConn,
+ idleTimeout: srv.IdleTimeout,
+ closeCanceler: cancel,
+ }
+ if srv.MaxTimeout > 0 {
+ conn.maxDeadline = time.Now().Add(srv.MaxTimeout)
+ }
+ if srv.HandshakeTimeout > 0 {
+ conn.handshakeDeadline = time.Now().Add(srv.HandshakeTimeout)
+ }
+ conn.updateDeadline()
+ defer func() { _ = conn.Close() }()
+ defer func() {
+ if srv.ConnectionCloseCallback != nil {
+ srv.ConnectionCloseCallback(conn)
+ }
+ }()
+ sshConn, chans, reqs, err := gossh.NewServerConn(conn, srv.config(ctx))
+ if err != nil {
+ if srv.ConnectionFailedCallback != nil {
+ srv.ConnectionFailedCallback(conn, err)
+ }
+ return
+ }
+ conn.handshakeDeadline = time.Time{}
+ conn.updateDeadline()
+
+ if err := extractPublicKeyFromPermissions(ctx, sshConn); err != nil {
+ if srv.ConnectionFailedCallback != nil {
+ srv.ConnectionFailedCallback(conn, err)
+ }
+ return
+ }
+
+ // Additionally, now that the connection was authed, we can take the
+ // permissions off of the gossh.Conn and re-attach them to the Permissions
+ // object stored in the Context.
+ ctx.Permissions().Permissions = sshConn.Permissions
+
+ srv.trackConn(sshConn, true)
+ defer srv.trackConn(sshConn, false)
+
+ ctx.SetValue(ContextKeyConn, sshConn)
+ applyConnMetadata(ctx, sshConn)
+ // go gossh.DiscardRequests(reqs)
+ go func() {
+ defer recoverAndLog("panic handling requests", conn, nil)
+ srv.handleRequests(ctx, reqs)
+ }()
+ for ch := range chans {
+ handler := srv.ChannelHandlers[ch.ChannelType()]
+ if handler == nil {
+ handler = srv.ChannelHandlers["default"]
+ }
+ if handler == nil {
+ _ = ch.Reject(gossh.UnknownChannelType, "unsupported channel type")
+ continue
+ }
+ go func(ch gossh.NewChannel) {
+ defer recoverAndLog("panic handling channel", conn, nil)
+ handler(srv, sshConn, ch, ctx)
+ }(ch)
+ }
+}
+
+func (srv *Server) handleRequests(ctx Context, in <-chan *gossh.Request) {
+ for req := range in {
+ handler := srv.RequestHandlers[req.Type]
+ if handler == nil {
+ handler = srv.RequestHandlers["default"]
+ }
+ if handler == nil {
+ _ = req.Reply(false, nil)
+ continue
+ }
+ /*reqCtx, cancel := context.WithCancel(ctx)
+ defer cancel() */
+ ret, payload := handler(ctx, srv, req)
+ _ = req.Reply(ret, payload)
+ }
+}
+
+// ListenAndServe listens on the TCP network address srv.Addr and then calls
+// Serve to handle incoming connections. If srv.Addr is blank, ":22" is used.
+// ListenAndServe always returns a non-nil error.
+func (srv *Server) ListenAndServe() error {
+ addr := srv.Addr
+ if addr == "" {
+ addr = ":22"
+ }
+ ln, err := net.Listen("tcp", addr)
+ if err != nil {
+ return err
+ }
+ return srv.Serve(ln)
+}
+
+// AddHostKey adds a private key as a host key. If an existing host key exists
+// with the same algorithm, it is overwritten. Each server config must have at
+// least one host key.
+func (srv *Server) AddHostKey(key Signer) {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ // these are later added via AddHostKey on ServerConfig, which performs the
+ // check for one of every algorithm.
+
+ // This check is based on the AddHostKey method from the x/crypto/ssh
+ // library. This allows us to only keep one active key for each type on a
+ // server at once. So, if you're dynamically updating keys at runtime, this
+ // list will not keep growing.
+ for i, k := range srv.HostSigners {
+ if k.PublicKey().Type() == key.PublicKey().Type() {
+ srv.HostSigners[i] = key
+ return
+ }
+ }
+
+ srv.HostSigners = append(srv.HostSigners, key)
+}
+
+// SetOption runs a functional option against the server.
+// It returns an error if the server has already been started.
+func (srv *Server) SetOption(option Option) error {
+ if srv.started.Load() {
+ return fmt.Errorf("ssh: cannot set option after server has started")
+ }
+
+ // NOTE: there is a potential race here for any option that doesn't call an
+ // internal method. We can't actually lock here because if something calls
+ // (as an example) AddHostKey, it will deadlock.
+
+ // srv.mu.Lock()
+ // defer srv.mu.Unlock()
+
+ return option(srv)
+}
+
+func (srv *Server) getDoneChan() <-chan struct{} {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ return srv.getDoneChanLocked()
+}
+
+func (srv *Server) getDoneChanLocked() chan struct{} {
+ if srv.doneChan == nil {
+ srv.doneChan = make(chan struct{})
+ }
+ return srv.doneChan
+}
+
+func (srv *Server) closeDoneChanLocked() {
+ ch := srv.getDoneChanLocked()
+ select {
+ case <-ch:
+ // Already closed. Don't close again.
+ default:
+ // Safe to close here. We're the only closer, guarded
+ // by srv.mu.
+ close(ch)
+ }
+}
+
+func (srv *Server) closeListenersLocked() error {
+ var err error
+ for ln := range srv.listeners {
+ if cerr := ln.Close(); cerr != nil && err == nil {
+ err = cerr
+ }
+ delete(srv.listeners, ln)
+ }
+ return err
+}
+
+func (srv *Server) trackListener(ln net.Listener, add bool) {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ if srv.listeners == nil {
+ srv.listeners = make(map[net.Listener]struct{})
+ }
+ if add {
+ // If the *Server is being reused after a previous
+ // Close or Shutdown, reset its doneChan:
+ if len(srv.listeners) == 0 && len(srv.conns) == 0 {
+ srv.doneChan = nil
+ }
+ srv.listeners[ln] = struct{}{}
+ srv.listenerWg.Add(1)
+ } else {
+ delete(srv.listeners, ln)
+ srv.listenerWg.Done()
+ }
+}
+
+func (srv *Server) trackConn(c *gossh.ServerConn, add bool) {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ if srv.conns == nil {
+ srv.conns = make(map[*gossh.ServerConn]struct{})
+ }
+ if add {
+ srv.conns[c] = struct{}{}
+ srv.connWg.Add(1)
+ } else {
+ delete(srv.conns, c)
+ srv.connWg.Done()
+ }
+}
+
+// extractPublicKeyFromPermissions re-parses the public key from the
+// permissions extensions and stores it in the context.
+func extractPublicKeyFromPermissions(ctx Context, sshConn *gossh.ServerConn) error {
+ if sshConn.Permissions == nil {
+ return nil
+ }
+ keyData, ok := sshConn.Permissions.Extensions[permissionsPublicKeyExt]
+ if !ok {
+ return nil
+ }
+ decodedData, err := base64.StdEncoding.DecodeString(keyData)
+ if err != nil {
+ return err
+ }
+ key, err := gossh.ParsePublicKey(decodedData)
+ if err != nil {
+ return err
+ }
+ ctx.SetValue(ContextKeyPublicKey, key)
+ return nil
+}
diff --git a/vendor/charm.land/ssh/session.go b/vendor/charm.land/ssh/session.go
new file mode 100644
index 0000000000..881b2e2848
--- /dev/null
+++ b/vendor/charm.land/ssh/session.go
@@ -0,0 +1,458 @@
+package ssh
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "log/slog"
+ "net"
+ "sync"
+
+ "github.com/anmitsu/go-shlex"
+ gossh "golang.org/x/crypto/ssh"
+)
+
+// Session provides access to information about an SSH session and methods
+// to read and write to the SSH channel with an embedded Channel interface from
+// crypto/ssh.
+//
+// When Command() returns an empty slice, the user requested a shell. Otherwise
+// the user is performing an exec with those command arguments.
+//
+// TODO: Signals.
+type Session interface {
+ gossh.Channel
+
+ // User returns the username used when establishing the SSH connection.
+ User() string
+
+ // RemoteAddr returns the net.Addr of the client side of the connection.
+ RemoteAddr() net.Addr
+
+ // LocalAddr returns the net.Addr of the server side of the connection.
+ LocalAddr() net.Addr
+
+ // Environ returns a copy of strings representing the environment set by the
+ // user for this session, in the form "key=value".
+ Environ() []string
+
+ // Exit sends an exit status and then closes the session.
+ Exit(code int) error
+
+ // Command returns a shell parsed slice of arguments that were provided by the
+ // user. Shell parsing splits the command string according to POSIX shell rules,
+ // which considers quoting not just whitespace.
+ Command() []string
+
+ // RawCommand returns the exact command that was provided by the user.
+ RawCommand() string
+
+ // Subsystem returns the subsystem requested by the user.
+ Subsystem() string
+
+ // PublicKey returns the PublicKey used to authenticate. If a public key was not
+ // used it will return nil.
+ PublicKey() PublicKey
+
+ // Context returns the connection's context. The returned context is always
+ // non-nil and holds the same data as the Context passed into auth
+ // handlers and callbacks.
+ //
+ // The context is canceled when the client's connection closes or I/O
+ // operation fails.
+ Context() Context
+
+ // Permissions returns a copy of the Permissions object that was available for
+ // setup in the auth handlers via the Context.
+ Permissions() Permissions
+
+ // EmulatedPty returns true if the session is emulating a PTY using PtyWriter.
+ EmulatedPty() bool
+
+ // Pty returns PTY information, a channel of window size changes, and a boolean
+ // of whether or not a PTY was accepted for this session.
+ Pty() (Pty, <-chan Window, bool)
+
+ // Signals registers a channel to receive signals sent from the client. The
+ // channel must handle signal sends or it will block the SSH request loop.
+ // Registering nil will unregister the channel from signal sends. During the
+ // time no channel is registered signals are buffered up to a reasonable amount.
+ // If there are buffered signals when a channel is registered, they will be
+ // sent in order on the channel immediately after registering.
+ Signals(c chan<- Signal)
+
+ // Break regisers a channel to receive notifications of break requests sent
+ // from the client. The channel must handle break requests, or it will block
+ // the request handling loop. Registering nil will unregister the channel.
+ // During the time that no channel is registered, breaks are ignored.
+ Break(c chan<- bool)
+}
+
+// maxSigBufSize is how many signals will be buffered
+// when there is no signal channel specified.
+const maxSigBufSize = 128
+
+// DefaultSessionHandler is the default handler for the "session" channel type.
+func DefaultSessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
+ ch, reqs, err := newChan.Accept()
+ if err != nil {
+ slog.Warn("ssh: failed to accept session channel", "err", err)
+ return
+ }
+ sess := &session{
+ Channel: ch,
+ conn: conn,
+ handler: srv.Handler,
+ ptyCb: srv.PtyCallback,
+ ptyHandler: srv.PtyHandler,
+ sessReqCb: srv.SessionRequestCallback,
+ subsystemHandlers: srv.SubsystemHandlers,
+ ctx: ctx,
+ }
+ ctx.SetValue(ContextKeySession, sess)
+ sess.handleRequests(reqs)
+}
+
+type session struct {
+ sync.Mutex
+ gossh.Channel
+ conn *gossh.ServerConn
+ handler Handler
+ subsystemHandlers map[string]SubsystemHandler
+ handled bool
+ exited bool
+ pty *Pty
+ winch chan Window
+ env []string
+ ptyCb PtyCallback
+ ptyHandler PtyHandler
+ sessReqCb SessionRequestCallback
+ rawCmd string
+ subsystem string
+ ctx Context
+ sigCh chan<- Signal
+ sigBuf []Signal
+ breakCh chan<- bool
+}
+
+func (sess *session) Stderr() io.ReadWriter {
+ if sess.pty != nil && sess.EmulatedPty() {
+ return NewPtyReadWriter(sess.Channel.Stderr())
+ }
+ return sess.Channel.Stderr()
+}
+
+func (sess *session) Write(p []byte) (int, error) {
+ if sess.pty != nil && sess.EmulatedPty() {
+ return NewPtyWriter(sess.Channel).Write(p)
+ }
+ return sess.Channel.Write(p)
+}
+
+func (sess *session) PublicKey() PublicKey {
+ sessionkey := sess.ctx.Value(ContextKeyPublicKey)
+ if sessionkey == nil {
+ return nil
+ }
+ return sessionkey.(PublicKey)
+}
+
+func (sess *session) Permissions() Permissions {
+ // use context permissions because its properly
+ // wrapped and easier to dereference
+ perms := sess.ctx.Value(ContextKeyPermissions).(*Permissions)
+ return *perms
+}
+
+func (sess *session) Context() Context {
+ return sess.ctx
+}
+
+func (sess *session) Exit(code int) error {
+ sess.Lock()
+ defer sess.Unlock()
+ if sess.exited {
+ return errors.New("Session.Exit called multiple times")
+ }
+ sess.exited = true
+
+ status := struct{ Status uint32 }{uint32(code)}
+ _, err := sess.SendRequest("exit-status", false, gossh.Marshal(&status))
+ if err != nil {
+ return err
+ }
+ return sess.Close()
+}
+
+func (sess *session) User() string {
+ return sess.conn.User()
+}
+
+func (sess *session) RemoteAddr() net.Addr {
+ return sess.conn.RemoteAddr()
+}
+
+func (sess *session) LocalAddr() net.Addr {
+ return sess.conn.LocalAddr()
+}
+
+func (sess *session) Environ() []string {
+ return append([]string(nil), sess.env...)
+}
+
+func (sess *session) RawCommand() string {
+ return sess.rawCmd
+}
+
+func (sess *session) Command() []string {
+ cmd, _ := shlex.Split(sess.rawCmd, true)
+ return append([]string(nil), cmd...)
+}
+
+func (sess *session) Subsystem() string {
+ return sess.subsystem
+}
+
+func (sess *session) EmulatedPty() bool {
+ return sess.ctx.Value(contextKeyEmulatePty) == true
+}
+
+func (sess *session) Pty() (Pty, <-chan Window, bool) {
+ if sess.pty != nil && (sess.EmulatedPty() || !sess.pty.IsZero()) {
+ return *sess.pty, sess.winch, true
+ }
+ return Pty{}, sess.winch, false
+}
+
+func (sess *session) Signals(c chan<- Signal) {
+ sess.Lock()
+ defer sess.Unlock()
+ sess.sigCh = c
+ if len(sess.sigBuf) > 0 {
+ go func() {
+ for _, sig := range sess.sigBuf {
+ sess.sigCh <- sig
+ }
+ }()
+ }
+}
+
+func (sess *session) Break(c chan<- bool) {
+ sess.Lock()
+ defer sess.Unlock()
+ sess.breakCh = c
+}
+
+func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
+ for req := range reqs {
+ switch req.Type {
+ case "shell", "exec":
+ if sess.handled {
+ _ = req.Reply(false, nil)
+ continue
+ }
+
+ payload := struct{ Value string }{}
+ _ = gossh.Unmarshal(req.Payload, &payload)
+ sess.rawCmd = payload.Value
+
+ // If there's a session policy callback, we need to confirm before
+ // accepting the session.
+ if sess.sessReqCb != nil && !sess.sessReqCb(sess, req.Type) {
+ sess.rawCmd = ""
+ _ = req.Reply(false, nil)
+ continue
+ }
+
+ if sess.handler == nil {
+ _ = req.Reply(false, nil)
+ continue
+ }
+
+ sess.handled = true
+ _ = req.Reply(true, nil)
+
+ go func() {
+ // Closed from a defer so the pty is still released when the
+ // handler panics, rather than trading a crash for a leak.
+ if sess.pty != nil && !sess.pty.IsZero() {
+ defer func() { _ = sess.pty.Close() }()
+ }
+ defer recoverAndLog("panic in session handler", nil, func() {
+ _ = sess.Exit(1)
+ })
+ if sess.pty != nil && !sess.pty.IsZero() {
+ go func() {
+ defer recoverAndLog("panic copying to pty", nil, nil)
+ _, _ = io.Copy(sess.pty, sess)
+ }()
+ go func() {
+ defer recoverAndLog("panic copying from pty", nil, nil)
+ _, _ = io.Copy(sess, sess.pty)
+ }()
+ }
+ sess.handler(sess)
+ _ = sess.Exit(0)
+ }()
+ case "subsystem":
+ if sess.handled {
+ _ = req.Reply(false, nil)
+ continue
+ }
+
+ payload := struct{ Value string }{}
+ _ = gossh.Unmarshal(req.Payload, &payload)
+ sess.subsystem = payload.Value
+
+ // If there's a session policy callback, we need to confirm before
+ // accepting the session.
+ if sess.sessReqCb != nil && !sess.sessReqCb(sess, req.Type) {
+ sess.rawCmd = ""
+ _ = req.Reply(false, nil)
+ continue
+ }
+
+ handler := sess.subsystemHandlers[payload.Value]
+ if handler == nil {
+ handler = sess.subsystemHandlers["default"]
+ }
+ if handler == nil {
+ _ = req.Reply(false, nil)
+ continue
+ }
+
+ sess.handled = true
+ _ = req.Reply(true, nil)
+
+ go func() {
+ defer recoverAndLog("panic in subsystem handler", nil, func() {
+ _ = sess.Exit(1)
+ })
+ handler(sess)
+ _ = sess.Exit(0)
+ }()
+ case "env":
+ if sess.handled {
+ _ = req.Reply(false, nil)
+ continue
+ }
+ var kv struct{ Key, Value string }
+ _ = gossh.Unmarshal(req.Payload, &kv)
+ sess.env = append(sess.env, fmt.Sprintf("%s=%s", kv.Key, kv.Value))
+ _ = req.Reply(true, nil)
+ case "signal":
+ var payload struct{ Signal string }
+ _ = gossh.Unmarshal(req.Payload, &payload)
+ sess.Lock()
+ if sess.sigCh != nil {
+ sess.sigCh <- Signal(payload.Signal)
+ } else {
+ if len(sess.sigBuf) < maxSigBufSize {
+ sess.sigBuf = append(sess.sigBuf, Signal(payload.Signal))
+ }
+ }
+ sess.Unlock()
+ case "pty-req":
+ if sess.handled || sess.pty != nil {
+ _ = req.Reply(false, nil)
+ continue
+ }
+ ptyReq, ok := parsePtyRequest(req.Payload)
+ if !ok {
+ _ = req.Reply(false, nil)
+ continue
+ }
+ if sess.ptyCb != nil {
+ ok := sess.ptyCb(sess.ctx, ptyReq)
+ if !ok {
+ _ = req.Reply(false, nil)
+ continue
+ }
+ }
+
+ sess.pty = &ptyReq
+ sess.winch = make(chan Window, 1)
+ sess.winch <- ptyReq.Window
+
+ if sess.ptyHandler != nil {
+ closer, err := sess.ptyHandler(sess.ctx, sess, ptyReq)
+ if err != nil {
+ // TODO: handle error
+ _ = req.Reply(false, nil)
+ continue
+ }
+
+ defer func() { _ = closer() }() //nolint:staticcheck // intentional: runs when req channel closes
+
+ if !sess.EmulatedPty() && !sess.pty.IsZero() {
+ go func() {
+ defer recoverAndLog("panic resizing pty", nil, nil)
+ for win := range sess.winch {
+ if err := resizePty(sess, win); err != nil {
+ // TODO: handle error
+ continue
+ }
+ }
+ }()
+ }
+ }
+
+ defer func() { //nolint:staticcheck // intentional: runs when req channel closes
+ // when reqs is closed
+ close(sess.winch)
+ }()
+ _ = req.Reply(ok, nil)
+ case "window-change":
+ if sess.pty == nil {
+ _ = req.Reply(false, nil)
+ continue
+ }
+ win, _, ok := parseWindow(req.Payload)
+ if ok {
+ sess.pty.Window = win
+ sess.winch <- win
+ }
+ _ = req.Reply(ok, nil)
+ case agentRequestType:
+ // TODO: option/callback to allow agent forwarding
+ SetAgentRequested(sess.ctx)
+ _ = req.Reply(true, nil)
+ case "break":
+ ok := false
+ sess.Lock()
+ if sess.breakCh != nil {
+ sess.breakCh <- true
+ ok = true
+ }
+ _ = req.Reply(ok, nil)
+ sess.Unlock()
+ default:
+ slog.Debug("ssh: unknown session request", "type", req.Type)
+ _ = req.Reply(false, nil)
+ }
+ }
+}
+
+func (sess *session) ptyAllocate(term string, win Window, modes gossh.TerminalModes) (func() error, error) {
+ p, err := newPty(sess.ctx, term, win, modes)
+ if err != nil {
+ return nil, err
+ }
+
+ sess.pty = &Pty{
+ Term: term,
+ Window: win,
+ Modes: modes,
+ impl: p,
+ }
+
+ return p.Close, nil
+}
+
+func resizePty(sess *session, win Window) error {
+ if sess.pty == nil {
+ return nil
+ }
+
+ return sess.pty.Resize(win.Width, win.Height)
+}
diff --git a/vendor/charm.land/ssh/ssh.go b/vendor/charm.land/ssh/ssh.go
new file mode 100644
index 0000000000..a1d6030052
--- /dev/null
+++ b/vendor/charm.land/ssh/ssh.go
@@ -0,0 +1,166 @@
+package ssh
+
+import (
+ "crypto/subtle"
+ "net"
+
+ gossh "golang.org/x/crypto/ssh"
+)
+
+// Signal represents a POSIX signal as specified in RFC 4254 Section 6.10.
+type Signal string
+
+// POSIX signals as listed in RFC 4254 Section 6.10.
+const (
+ SIGABRT Signal = "ABRT"
+ SIGALRM Signal = "ALRM"
+ SIGFPE Signal = "FPE"
+ SIGHUP Signal = "HUP"
+ SIGILL Signal = "ILL"
+ SIGINT Signal = "INT"
+ SIGKILL Signal = "KILL"
+ SIGPIPE Signal = "PIPE"
+ SIGQUIT Signal = "QUIT"
+ SIGSEGV Signal = "SEGV"
+ SIGTERM Signal = "TERM"
+ SIGUSR1 Signal = "USR1"
+ SIGUSR2 Signal = "USR2"
+)
+
+// DefaultHandler is the default Handler used by Serve.
+var DefaultHandler Handler
+
+// Option is a functional option handler for Server.
+type Option func(*Server) error
+
+// Handler is a callback for handling established SSH sessions.
+type Handler func(Session)
+
+// BannerHandler is a callback for displaying the server banner.
+type BannerHandler func(ctx Context) string
+
+// PublicKeyHandler is a callback for performing public key authentication.
+type PublicKeyHandler func(ctx Context, key PublicKey) bool
+
+// PasswordHandler is a callback for performing password authentication.
+type PasswordHandler func(ctx Context, password string) bool
+
+// KeyboardInteractiveHandler is a callback for performing keyboard-interactive authentication.
+type KeyboardInteractiveHandler func(ctx Context, challenger gossh.KeyboardInteractiveChallenge) bool
+
+// PtyHandler is a callback for handling PTY allocation requests.
+type PtyHandler func(ctx Context, s Session, pty Pty) (func() error, error)
+
+// PtyCallback is a hook for handling PTY allocation requests.
+type PtyCallback func(ctx Context, req Pty) bool
+
+// SessionRequestCallback is a callback for allowing or denying SSH sessions.
+type SessionRequestCallback func(sess Session, requestType string) bool
+
+// ConnCallback is a hook for new connections before handling.
+// It allows wrapping for timeouts and limiting by returning
+// the net.Conn that will be used as the underlying connection.
+type ConnCallback func(ctx Context, conn net.Conn) net.Conn
+
+// LocalPortForwardingCallback is a hook for allowing port forwarding.
+type LocalPortForwardingCallback func(ctx Context, destinationHost string, destinationPort uint32) bool
+
+// ReversePortForwardingCallback is a hook for allowing reverse port forwarding.
+type ReversePortForwardingCallback func(ctx Context, bindHost string, bindPort uint32) bool
+
+// ServerConfigCallback is a hook for creating custom default server configs.
+type ServerConfigCallback func(ctx Context) *gossh.ServerConfig
+
+// ConnectionFailedCallback is a hook for reporting failed connections
+// Please note: the net.Conn is likely to be closed at this point.
+type ConnectionFailedCallback func(conn net.Conn, err error)
+
+// ConnectionCloseCallback is a hook for reporting closed connections.
+type ConnectionCloseCallback func(conn net.Conn)
+
+// Window represents the size of a PTY window.
+//
+// From https://datatracker.ietf.org/doc/html/rfc4254#section-6.2
+//
+// Zero dimension parameters MUST be ignored. The character/row dimensions
+// override the pixel dimensions (when nonzero). Pixel dimensions refer
+// to the drawable area of the window.
+type Window struct {
+ // Width is the number of columns.
+ // It overrides WidthPixels.
+ Width int
+ // Height is the number of rows.
+ // It overrides HeightPixels.
+ Height int
+
+ // WidthPixels is the drawable width of the window, in pixels.
+ WidthPixels int
+ // HeightPixels is the drawable height of the window, in pixels.
+ HeightPixels int
+}
+
+// Pty represents a PTY request and configuration.
+type Pty struct {
+ impl
+
+ // Term is the TERM environment variable value.
+ Term string
+
+ // Window is the Window sent as part of the pty-req.
+ Window Window
+
+ // Modes represent a mapping of Terminal Mode opcode to value as it was
+ // requested by the client as part of the pty-req. These are outlined as
+ // part of https://datatracker.ietf.org/doc/html/rfc4254#section-8.
+ //
+ // The opcodes are defined as constants in golang.org/x/crypto/ssh (VINTR,VQUIT,etc.).
+ // Boolean opcodes have values 0 or 1.
+ //
+ // Note: golang.org/x/crypto/ssh currently (2022-03-12) doesn't have a
+ // definition for opcode 42 "iutf8" which was introduced in https://datatracker.ietf.org/doc/html/rfc8160.
+ Modes gossh.TerminalModes
+}
+
+// Serve accepts incoming SSH connections on the listener l, creating a new
+// connection goroutine for each. The connection goroutines read requests and
+// then calls handler to handle sessions. Handler is typically nil, in which
+// case the DefaultHandler is used.
+func Serve(l net.Listener, handler Handler, options ...Option) error {
+ srv := &Server{Handler: handler}
+ for _, option := range options {
+ if err := srv.SetOption(option); err != nil {
+ return err
+ }
+ }
+ return srv.Serve(l)
+}
+
+// ListenAndServe listens on the TCP network address addr and then calls Serve
+// with handler to handle sessions on incoming connections. Handler is typically
+// nil, in which case the DefaultHandler is used.
+func ListenAndServe(addr string, handler Handler, options ...Option) error {
+ srv := &Server{Addr: addr, Handler: handler}
+ for _, option := range options {
+ if err := srv.SetOption(option); err != nil {
+ return err
+ }
+ }
+ return srv.ListenAndServe()
+}
+
+// Handle registers the handler as the DefaultHandler.
+func Handle(handler Handler) {
+ DefaultHandler = handler
+}
+
+// KeysEqual is constant time compare of the keys to avoid timing attacks.
+func KeysEqual(ak, bk PublicKey) bool {
+ // avoid panic if one of the keys is nil, return false instead
+ if ak == nil || bk == nil {
+ return false
+ }
+
+ a := ak.Marshal()
+ b := bk.Marshal()
+ return (len(a) == len(b) && subtle.ConstantTimeCompare(a, b) == 1)
+}
diff --git a/vendor/charm.land/ssh/tcpip.go b/vendor/charm.land/ssh/tcpip.go
new file mode 100644
index 0000000000..aacf27ca94
--- /dev/null
+++ b/vendor/charm.land/ssh/tcpip.go
@@ -0,0 +1,204 @@
+package ssh
+
+import (
+ "io"
+ "log/slog"
+ "net"
+ "strconv"
+ "sync"
+
+ gossh "golang.org/x/crypto/ssh"
+)
+
+const (
+ forwardedTCPChannelType = "forwarded-tcpip"
+)
+
+// direct-tcpip data struct as specified in RFC4254, Section 7.2.
+type localForwardChannelData struct {
+ DestAddr string
+ DestPort uint32
+
+ OriginAddr string
+ OriginPort uint32
+}
+
+// DirectTCPIPHandler can be enabled by adding it to the server's
+// ChannelHandlers under direct-tcpip.
+func DirectTCPIPHandler(srv *Server, _ *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
+ d := localForwardChannelData{}
+ if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
+ _ = newChan.Reject(gossh.ConnectionFailed, "error parsing forward data: "+err.Error())
+ return
+ }
+
+ if srv.LocalPortForwardingCallback == nil || !srv.LocalPortForwardingCallback(ctx, d.DestAddr, d.DestPort) {
+ _ = newChan.Reject(gossh.Prohibited, "port forwarding is disabled")
+ return
+ }
+
+ dest := net.JoinHostPort(d.DestAddr, strconv.FormatInt(int64(d.DestPort), 10))
+
+ var dialer net.Dialer
+ dconn, err := dialer.DialContext(ctx, "tcp", dest)
+ if err != nil {
+ _ = newChan.Reject(gossh.ConnectionFailed, err.Error())
+ return
+ }
+
+ ch, reqs, err := newChan.Accept()
+ if err != nil {
+ _ = dconn.Close()
+ return
+ }
+ go gossh.DiscardRequests(reqs)
+
+ go func() {
+ defer recoverAndLog("panic proxying forwarded connection", nil, nil)
+ defer func() { _ = ch.Close() }()
+ defer func() { _ = dconn.Close() }()
+ _, _ = io.Copy(ch, dconn)
+ }()
+ go func() {
+ defer recoverAndLog("panic proxying forwarded connection", nil, nil)
+ defer func() { _ = ch.Close() }()
+ defer func() { _ = dconn.Close() }()
+ _, _ = io.Copy(dconn, ch)
+ }()
+}
+
+type remoteForwardRequest struct {
+ BindAddr string
+ BindPort uint32
+}
+
+type remoteForwardSuccess struct {
+ BindPort uint32
+}
+
+type remoteForwardCancelRequest struct {
+ BindAddr string
+ BindPort uint32
+}
+
+type remoteForwardChannelData struct {
+ DestAddr string
+ DestPort uint32
+ OriginAddr string
+ OriginPort uint32
+}
+
+// ForwardedTCPHandler can be enabled by creating a ForwardedTCPHandler and
+// adding the HandleSSHRequest callback to the server's RequestHandlers under
+// tcpip-forward and cancel-tcpip-forward.
+type ForwardedTCPHandler struct {
+ forwards map[string]net.Listener
+ sync.Mutex
+}
+
+// HandleSSHRequest handles the tcpip-forward and cancel-tcpip-forward
+// global requests.
+func (h *ForwardedTCPHandler) HandleSSHRequest(ctx Context, srv *Server, req *gossh.Request) (bool, []byte) {
+ h.Lock()
+ if h.forwards == nil {
+ h.forwards = make(map[string]net.Listener)
+ }
+ h.Unlock()
+ conn := ctx.Value(ContextKeyConn).(*gossh.ServerConn)
+ switch req.Type {
+ case "tcpip-forward":
+ var reqPayload remoteForwardRequest
+ if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil {
+ slog.Warn("ssh: failed to parse tcpip-forward request", "err", err)
+ return false, []byte{}
+ }
+ if srv.ReversePortForwardingCallback == nil || !srv.ReversePortForwardingCallback(ctx, reqPayload.BindAddr, reqPayload.BindPort) {
+ return false, []byte("port forwarding is disabled")
+ }
+ addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort)))
+ ln, err := net.Listen("tcp", addr)
+ if err != nil {
+ slog.Warn("ssh: reverse port forward listen failed", "addr", addr, "err", err)
+ return false, []byte{}
+ }
+ _, destPortStr, _ := net.SplitHostPort(ln.Addr().String())
+ destPort, _ := strconv.Atoi(destPortStr)
+ // Use the actual bound port as the map key so that port 0
+ // requests don't collide.
+ addr = net.JoinHostPort(reqPayload.BindAddr, destPortStr)
+ h.Lock()
+ h.forwards[addr] = ln
+ h.Unlock()
+ go func() {
+ <-ctx.Done()
+ h.Lock()
+ ln, ok := h.forwards[addr]
+ h.Unlock()
+ if ok {
+ _ = ln.Close()
+ }
+ }()
+ go func() {
+ for {
+ c, err := ln.Accept()
+ if err != nil {
+ slog.Debug("ssh: reverse port forward accept failed", "addr", addr, "err", err)
+ break
+ }
+ originAddr, orignPortStr, _ := net.SplitHostPort(c.RemoteAddr().String())
+ originPort, _ := strconv.Atoi(orignPortStr)
+ payload := gossh.Marshal(&remoteForwardChannelData{
+ DestAddr: reqPayload.BindAddr,
+ DestPort: uint32(destPort),
+ OriginAddr: originAddr,
+ OriginPort: uint32(originPort),
+ })
+ go func() {
+ defer recoverAndLog("panic opening forwarded channel", nil, func() {
+ _ = c.Close()
+ })
+ ch, reqs, err := conn.OpenChannel(forwardedTCPChannelType, payload)
+ if err != nil {
+ slog.Warn("ssh: failed to open forwarded channel", "err", err)
+ _ = c.Close()
+ return
+ }
+ go gossh.DiscardRequests(reqs)
+ go func() {
+ defer recoverAndLog("panic proxying forwarded channel", nil, nil)
+ defer func() { _ = ch.Close() }()
+ defer func() { _ = c.Close() }()
+ _, _ = io.Copy(ch, c)
+ }()
+ go func() {
+ defer recoverAndLog("panic proxying forwarded channel", nil, nil)
+ defer func() { _ = ch.Close() }()
+ defer func() { _ = c.Close() }()
+ _, _ = io.Copy(c, ch)
+ }()
+ }()
+ }
+ h.Lock()
+ delete(h.forwards, addr)
+ h.Unlock()
+ }()
+ return true, gossh.Marshal(&remoteForwardSuccess{uint32(destPort)})
+
+ case "cancel-tcpip-forward":
+ var reqPayload remoteForwardCancelRequest
+ if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil {
+ slog.Warn("ssh: failed to parse cancel-tcpip-forward request", "err", err)
+ return false, []byte{}
+ }
+ addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort)))
+ h.Lock()
+ ln, ok := h.forwards[addr]
+ h.Unlock()
+ if ok {
+ _ = ln.Close()
+ }
+ return true, nil
+ default:
+ return false, nil
+ }
+}
diff --git a/vendor/charm.land/ssh/util.go b/vendor/charm.land/ssh/util.go
new file mode 100644
index 0000000000..2609c1fdd9
--- /dev/null
+++ b/vendor/charm.land/ssh/util.go
@@ -0,0 +1,156 @@
+package ssh
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "encoding/binary"
+
+ "golang.org/x/crypto/ssh"
+)
+
+func generateSigner() (ssh.Signer, error) {
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ return nil, err
+ }
+ return ssh.NewSignerFromKey(key)
+}
+
+func parsePtyRequest(payload []byte) (pty Pty, ok bool) {
+ // From https://datatracker.ietf.org/doc/html/rfc4254
+ // 6.2. Requesting a Pseudo-Terminal
+ // A pseudo-terminal can be allocated for the session by sending the
+ // following message.
+ // byte SSH_MSG_CHANNEL_REQUEST
+ // uint32 recipient channel
+ // string "pty-req"
+ // boolean want_reply
+ // string TERM environment variable value (e.g., vt100)
+ // uint32 terminal width, characters (e.g., 80)
+ // uint32 terminal height, rows (e.g., 24)
+ // uint32 terminal width, pixels (e.g., 640)
+ // uint32 terminal height, pixels (e.g., 480)
+ // string encoded terminal modes
+
+ // The payload starts from the TERM variable.
+ term, rem, ok := parseString(payload)
+ if !ok {
+ return pty, ok
+ }
+ win, rem, ok := parseWindow(rem)
+ if !ok {
+ return pty, ok
+ }
+ modes, ok := parseTerminalModes(rem)
+ if !ok {
+ return pty, ok
+ }
+ pty = Pty{
+ Term: term,
+ Window: win,
+ Modes: modes,
+ }
+ return pty, ok
+}
+
+func parseTerminalModes(in []byte) (modes ssh.TerminalModes, ok bool) {
+ // From https://datatracker.ietf.org/doc/html/rfc4254
+ // 8. Encoding of Terminal Modes
+ //
+ // All 'encoded terminal modes' (as passed in a pty request) are encoded
+ // into a byte stream. It is intended that the coding be portable
+ // across different environments. The stream consists of opcode-
+ // argument pairs wherein the opcode is a byte value. Opcodes 1 to 159
+ // have a single uint32 argument. Opcodes 160 to 255 are not yet
+ // defined, and cause parsing to stop (they should only be used after
+ // any other data). The stream is terminated by opcode TTY_OP_END
+ // (0x00).
+ //
+ // The client SHOULD put any modes it knows about in the stream, and the
+ // server MAY ignore any modes it does not know about. This allows some
+ // degree of machine-independence, at least between systems that use a
+ // POSIX-like tty interface. The protocol can support other systems as
+ // well, but the client may need to fill reasonable values for a number
+ // of parameters so the server pty gets set to a reasonable mode (the
+ // server leaves all unspecified mode bits in their default values, and
+ // only some combinations make sense).
+ _, rem, ok := parseUint32(in)
+ if !ok {
+ return modes, ok
+ }
+ const ttyOpEnd = 0
+ for len(rem) > 0 {
+ if modes == nil {
+ modes = make(ssh.TerminalModes)
+ }
+ code := rem[0]
+ rem = rem[1:]
+ if code == ttyOpEnd || code > 160 {
+ break
+ }
+ var val uint32
+ val, rem, ok = parseUint32(rem)
+ if !ok {
+ return modes, ok
+ }
+ modes[code] = val
+ }
+ ok = true
+ return modes, ok
+}
+
+func parseWindow(s []byte) (win Window, rem []byte, ok bool) {
+ // 6.7. Window Dimension Change Message
+ // When the window (terminal) size changes on the client side, it MAY
+ // send a message to the other side to inform it of the new dimensions.
+
+ // byte SSH_MSG_CHANNEL_REQUEST
+ // uint32 recipient channel
+ // string "window-change"
+ // boolean FALSE
+ // uint32 terminal width, columns
+ // uint32 terminal height, rows
+ // uint32 terminal width, pixels
+ // uint32 terminal height, pixels
+ wCols, rem, ok := parseUint32(s)
+ if !ok {
+ return win, rem, ok
+ }
+ hRows, rem, ok := parseUint32(rem)
+ if !ok {
+ return win, rem, ok
+ }
+ wPixels, rem, ok := parseUint32(rem)
+ if !ok {
+ return win, rem, ok
+ }
+ hPixels, rem, ok := parseUint32(rem)
+ if !ok {
+ return win, rem, ok
+ }
+ win = Window{
+ Width: int(wCols),
+ Height: int(hRows),
+ WidthPixels: int(wPixels),
+ HeightPixels: int(hPixels),
+ }
+ return win, rem, ok
+}
+
+func parseString(in []byte) (out string, rem []byte, ok bool) {
+ length, rem, ok := parseUint32(in)
+ if uint32(len(rem)) < length || !ok {
+ ok = false
+ return
+ }
+ out, rem = string(rem[:length]), rem[length:]
+ ok = true
+ return
+}
+
+func parseUint32(in []byte) (uint32, []byte, bool) {
+ if len(in) < 4 {
+ return 0, nil, false
+ }
+ return binary.BigEndian.Uint32(in), in[4:], true
+}
diff --git a/vendor/charm.land/ssh/wrap.go b/vendor/charm.land/ssh/wrap.go
new file mode 100644
index 0000000000..d1f2b161e6
--- /dev/null
+++ b/vendor/charm.land/ssh/wrap.go
@@ -0,0 +1,33 @@
+package ssh
+
+import gossh "golang.org/x/crypto/ssh"
+
+// PublicKey is an abstraction of different types of public keys.
+type PublicKey interface {
+ gossh.PublicKey
+}
+
+// The Permissions type holds fine-grained permissions that are specific to a
+// user or a specific authentication method for a user. Permissions, except for
+// "source-address", must be enforced in the server application layer, after
+// successful authentication.
+type Permissions struct {
+ *gossh.Permissions
+}
+
+// A Signer can create signatures that verify against a public key.
+type Signer interface {
+ gossh.Signer
+}
+
+// ParseAuthorizedKey parses a public key from an authorized_keys file used in
+// OpenSSH according to the sshd(8) manual page.
+func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, err error) {
+ return gossh.ParseAuthorizedKey(in)
+}
+
+// ParsePublicKey parses an SSH public key formatted for use in
+// the SSH wire protocol according to RFC 4253, section 6.6.
+func ParsePublicKey(in []byte) (out PublicKey, err error) {
+ return gossh.ParsePublicKey(in)
+}
diff --git a/vendor/charm.land/wish/v2/.golangci.yml b/vendor/charm.land/wish/v2/.golangci.yml
index b2fe88f448..c90f031611 100644
--- a/vendor/charm.land/wish/v2/.golangci.yml
+++ b/vendor/charm.land/wish/v2/.golangci.yml
@@ -30,12 +30,12 @@ linters:
- text: '(slog|log)\.\w+'
linters:
- noctx
- - text: "var-naming"
- linters:
- - revive
generated: lax
presets:
- common-false-positives
+ settings:
+ exhaustive:
+ default-signifies-exhaustive: true
issues:
max-issues-per-linter: 0
max-same-issues: 0
diff --git a/vendor/charm.land/wish/v2/README.md b/vendor/charm.land/wish/v2/README.md
index 8002510124..4bbcc0f66f 100644
--- a/vendor/charm.land/wish/v2/README.md
+++ b/vendor/charm.land/wish/v2/README.md
@@ -39,7 +39,7 @@ It is a cryptographic network protocol for operating network services securely o
That means, among other things, that we can write custom SSH servers without touching `openssh-server`,
so we can securely do more things than just providing a shell.
-Wish is a library that helps writing these kind of apps using Go.
+Wish is a library that helps writing these kinds of apps using Go.
## Middleware
@@ -95,8 +95,18 @@ and [Git server](examples/git) in the [examples](examples) folder.
* [Soft Serve](https://github.com/charmbracelet/soft-serve)
* [Wishlist](https://github.com/charmbracelet/wishlist)
+* [VHS](https://github.com/charmbracelet/vhs)
* [SSHWordle](https://github.com/davidcroda/sshwordle)
* [clidle](https://github.com/ajeetdsouza/clidle)
+* [pico.sh](https://pico.sh)
+* [Fztea](https://github.com/jon4hz/fztea)
+* [SSH Slides](https://github.com/ivantsepp/ssh-slides)
+* [Charming Slack](https://github.com/taciturnaxolotl/charming-slack)
+* [fissh](https://github.com/breqdev/fissh.breq.dev)
+* [Battlesship](https://github.com/JavierPoduje/battlesship)
+* [Purdue Hackers](https://github.com/purduehackers/organize)
+* [Go-Jitsu](https://github.com/noetarbouriech/go-jitsu)
+* [Matt Gleich's terminal](https://github.com/gleich/terminal)
* [ssh-warm-welcome](https://git.coopcloud.tech/decentral1se/ssh-warm-welcome)
[bubbletea]: https://github.com/charmbracelet/bubbletea
diff --git a/vendor/charm.land/wish/v2/Taskfile.yaml b/vendor/charm.land/wish/v2/Taskfile.yaml
new file mode 100644
index 0000000000..aa5860f4c6
--- /dev/null
+++ b/vendor/charm.land/wish/v2/Taskfile.yaml
@@ -0,0 +1,53 @@
+# https://taskfile.dev
+
+version: "3"
+
+tasks:
+ fmt:
+ desc: Run gofumpt and goimports
+ cmds:
+ - golangci-lint fmt
+
+ lint:
+ desc: Run base linters
+ cmds:
+ - golangci-lint run
+
+ lint:fix:
+ desc: Run linters and apply automatic fixes
+ cmds:
+ - golangci-lint run --fix
+
+ test:
+ desc: Run tests
+ cmds:
+ - go test -race ./... {{.CLI_ARGS}}
+
+ tidy:
+ desc: Run go mod tidy
+ cmds:
+ - go mod tidy
+
+ release:
+ desc: Create and push a new tag following semver
+ vars:
+ NEXT:
+ sh: svu next --always || go run github.com/caarlos0/svu/v3@latest next --always
+ prompt: "This will release {{.NEXT}}. Continue?"
+ preconditions:
+ - sh: '[ $(git symbolic-ref --short HEAD) = "main" ]'
+ msg: Not on main branch
+ - sh: "[ $(git status --porcelain=2 | wc -l) = 0 ]"
+ msg: "Git is dirty"
+ - sh: 'gh run list --workflow build.yml --commit $(git rev-parse HEAD) --status success --json conclusion -q ".[0].conclusion" | grep -q success'
+ msg: "Test build for this commit failed or not present"
+ cmds:
+ - task: fetch-tags
+ - git commit --allow-empty -m "{{.NEXT}}"
+ - git tag --annotate --sign -m "{{.NEXT}}" {{.NEXT}} {{.CLI_ARGS}}
+ - echo "Pushing {{.NEXT}}..."
+ - git push origin main --follow-tags
+
+ fetch-tags:
+ cmds:
+ - git fetch --tags
diff --git a/vendor/charm.land/wish/v2/activeterm/activeterm.go b/vendor/charm.land/wish/v2/activeterm/activeterm.go
index 9af7ae9df8..3f3e49a162 100644
--- a/vendor/charm.land/wish/v2/activeterm/activeterm.go
+++ b/vendor/charm.land/wish/v2/activeterm/activeterm.go
@@ -2,8 +2,8 @@
package activeterm
import (
+ "charm.land/ssh"
"charm.land/wish/v2"
- "github.com/charmbracelet/ssh"
)
// Middleware will exit 1 connections trying with no active terminals.
diff --git a/vendor/charm.land/wish/v2/bubbletea/tea.go b/vendor/charm.land/wish/v2/bubbletea/tea.go
index e93fe523d6..33443f5285 100644
--- a/vendor/charm.land/wish/v2/bubbletea/tea.go
+++ b/vendor/charm.land/wish/v2/bubbletea/tea.go
@@ -3,11 +3,12 @@ package bubbletea
import (
"context"
+ "runtime/debug"
tea "charm.land/bubbletea/v2"
"charm.land/log/v2"
+ "charm.land/ssh"
"charm.land/wish/v2"
- "github.com/charmbracelet/ssh"
)
// BubbleTeaHandler is the function Bubble Tea apps implement to hook into the
@@ -64,6 +65,22 @@ func MiddlewareWithProgramHandler(handler ProgramHandler) wish.Middleware {
}
ctx, cancel := context.WithCancel(sess.Context())
go func() {
+ // This runs on its own goroutine, so the recover middleware
+ // cannot see it: a recover only catches panics unwinding its
+ // own stack. Without this, a panic here would take down the
+ // whole server process rather than this one session.
+ //
+ // Quit the program on the way out, so the session ends instead
+ // of continuing with a window that no longer resizes.
+ defer func() {
+ if r := recover(); r != nil {
+ log.Error("panic in window change handler",
+ "panic", r,
+ "stack", string(debug.Stack()),
+ )
+ program.Quit()
+ }
+ }()
for {
select {
case <-ctx.Done():
diff --git a/vendor/charm.land/wish/v2/bubbletea/tea_other.go b/vendor/charm.land/wish/v2/bubbletea/tea_other.go
index d02c460be4..f0d2ab91b3 100644
--- a/vendor/charm.land/wish/v2/bubbletea/tea_other.go
+++ b/vendor/charm.land/wish/v2/bubbletea/tea_other.go
@@ -5,7 +5,7 @@ package bubbletea
import (
tea "charm.land/bubbletea/v2"
- "github.com/charmbracelet/ssh"
+ "charm.land/ssh"
)
func makeOpts(s ssh.Session) []tea.ProgramOption {
diff --git a/vendor/charm.land/wish/v2/bubbletea/tea_unix.go b/vendor/charm.land/wish/v2/bubbletea/tea_unix.go
index 49de890453..b9cdc8d9e1 100644
--- a/vendor/charm.land/wish/v2/bubbletea/tea_unix.go
+++ b/vendor/charm.land/wish/v2/bubbletea/tea_unix.go
@@ -5,8 +5,8 @@ package bubbletea
import (
tea "charm.land/bubbletea/v2"
+ "charm.land/ssh"
"github.com/charmbracelet/colorprofile"
- "github.com/charmbracelet/ssh"
)
func makeOpts(s ssh.Session) []tea.ProgramOption {
diff --git a/vendor/charm.land/wish/v2/cmd.go b/vendor/charm.land/wish/v2/cmd.go
index 50b3edd8b0..1f80c3a0a0 100644
--- a/vendor/charm.land/wish/v2/cmd.go
+++ b/vendor/charm.land/wish/v2/cmd.go
@@ -8,7 +8,7 @@ import (
"os/exec"
tea "charm.land/bubbletea/v2"
- "github.com/charmbracelet/ssh"
+ "charm.land/ssh"
)
// CommandContext is like Command but includes a context.
@@ -17,7 +17,7 @@ import (
// itself.
func CommandContext(ctx context.Context, s ssh.Session, name string, args ...string) *Cmd {
cmd := exec.CommandContext(ctx, name, args...)
- return &Cmd{s, cmd}
+ return &Cmd{sess: s, cmd: cmd}
}
// Command sets stdin, stdout, and stderr to the current session's PTY.
@@ -32,8 +32,11 @@ func Command(s ssh.Session, name string, args ...string) *Cmd {
// Cmd wraps a *exec.Cmd and a ssh.Pty so a command can be properly run.
type Cmd struct {
- sess ssh.Session
- cmd *exec.Cmd
+ sess ssh.Session
+ cmd *exec.Cmd
+ stdin io.Reader
+ stdout io.Writer
+ stderr io.Writer
}
// SetEnv sets the underlying exec.Cmd env.
@@ -53,9 +56,22 @@ func (c *Cmd) SetDir(dir string) {
// Run runs the program and waits for it to finish.
func (c *Cmd) Run() error {
+ if c.hasCustomStdio() {
+ c.cmd.Stdin, c.cmd.Stdout, c.cmd.Stderr = c.stdin, c.stdout, c.stderr
+ return c.cmd.Run() //nolint:wrapcheck
+ }
ppty, winCh, ok := c.sess.Pty()
if !ok {
c.cmd.Stdin, c.cmd.Stdout, c.cmd.Stderr = c.sess, c.sess, c.sess.Stderr()
+ if c.stdin != nil {
+ c.cmd.Stdin = c.stdin
+ }
+ if c.stdout != nil {
+ c.cmd.Stdout = c.stdout
+ }
+ if c.stderr != nil {
+ c.cmd.Stderr = c.stderr
+ }
if err := c.cmd.Run(); err != nil {
return fmt.Errorf("run command: %w", err)
}
@@ -64,13 +80,30 @@ func (c *Cmd) Run() error {
return c.doRun(ppty, winCh)
}
+// hasCustomStdio reports whether all three stdio handles were set, e.g. by
+// tea.Exec. The gate is all-or-nothing: a partial set keeps the default
+// session/PTY wiring, since ppty.Start cannot mix custom handles with the
+// PTY slave.
+func (c *Cmd) hasCustomStdio() bool {
+ return c.stdin != nil && c.stdout != nil && c.stderr != nil
+}
+
var _ tea.ExecCommand = &Cmd{}
-// SetStderr conforms with tea.ExecCommand.
-func (*Cmd) SetStderr(io.Writer) {}
+// SetStderr conforms with tea.ExecCommand. It must be called before Run;
+// Cmd is not safe for concurrent use.
+func (c *Cmd) SetStderr(w io.Writer) {
+ c.stderr = w
+}
-// SetStdin conforms with tea.ExecCommand.
-func (*Cmd) SetStdin(io.Reader) {}
+// SetStdin conforms with tea.ExecCommand. It must be called before Run;
+// Cmd is not safe for concurrent use.
+func (c *Cmd) SetStdin(r io.Reader) {
+ c.stdin = r
+}
-// SetStdout conforms with tea.ExecCommand.
-func (*Cmd) SetStdout(io.Writer) {}
+// SetStdout conforms with tea.ExecCommand. It must be called before Run;
+// Cmd is not safe for concurrent use.
+func (c *Cmd) SetStdout(w io.Writer) {
+ c.stdout = w
+}
diff --git a/vendor/charm.land/wish/v2/cmd_unix.go b/vendor/charm.land/wish/v2/cmd_unix.go
index 3b7e28c637..f87a3b41c5 100644
--- a/vendor/charm.land/wish/v2/cmd_unix.go
+++ b/vendor/charm.land/wish/v2/cmd_unix.go
@@ -3,7 +3,7 @@
package wish
-import "github.com/charmbracelet/ssh"
+import "charm.land/ssh"
func (c *Cmd) doRun(ppty ssh.Pty, _ <-chan ssh.Window) error {
if err := ppty.Start(c.cmd); err != nil {
diff --git a/vendor/charm.land/wish/v2/cmd_windows.go b/vendor/charm.land/wish/v2/cmd_windows.go
index f275337e64..b58f7ab29c 100644
--- a/vendor/charm.land/wish/v2/cmd_windows.go
+++ b/vendor/charm.land/wish/v2/cmd_windows.go
@@ -4,26 +4,15 @@
package wish
import (
- "fmt"
- "time"
-
- "github.com/charmbracelet/ssh"
+ "charm.land/ssh"
+ "github.com/charmbracelet/x/xpty"
)
func (c *Cmd) doRun(ppty ssh.Pty, _ <-chan ssh.Window) error {
if err := ppty.Start(c.cmd); err != nil {
return err //nolint:wrapcheck
}
-
- start := time.Now()
- for c.cmd.ProcessState == nil {
- if time.Since(start) > time.Second*10 {
- return fmt.Errorf("could not start process")
- }
- time.Sleep(100 * time.Millisecond)
- }
- if !c.cmd.ProcessState.Success() {
- return fmt.Errorf("process failed: exit %d", c.cmd.ProcessState.ExitCode())
- }
- return nil
+ // cmd.Wait() doesn't work with ConPTY; xpty.WaitProcess waits on the
+ // process directly and honors the session context for cancellation.
+ return xpty.WaitProcess(c.sess.Context(), c.cmd) //nolint:wrapcheck
}
diff --git a/vendor/charm.land/wish/v2/options.go b/vendor/charm.land/wish/v2/options.go
index dffb25a73c..09789e53d6 100644
--- a/vendor/charm.land/wish/v2/options.go
+++ b/vendor/charm.land/wish/v2/options.go
@@ -11,8 +11,8 @@ import (
"time"
"charm.land/log/v2"
+ "charm.land/ssh"
"github.com/charmbracelet/keygen"
- "github.com/charmbracelet/ssh"
gossh "golang.org/x/crypto/ssh"
)
@@ -32,7 +32,7 @@ func WithVersion(version string) ssh.Option {
}
}
-// WithBanner return an ssh.Option that sets the server banner.
+// WithBanner returns an ssh.Option that sets the server banner.
func WithBanner(banner string) ssh.Option {
return func(s *ssh.Server) error {
s.Banner = banner
@@ -40,7 +40,7 @@ func WithBanner(banner string) ssh.Option {
}
}
-// WithBannerHandler return an ssh.Option that sets the server banner handler,
+// WithBannerHandler returns an ssh.Option that sets the server banner handler,
// overriding WithBanner.
func WithBannerHandler(h ssh.BannerHandler) ssh.Option {
return func(s *ssh.Server) error {
diff --git a/vendor/charm.land/wish/v2/wish.go b/vendor/charm.land/wish/v2/wish.go
index d263ecd156..6c38d1485b 100644
--- a/vendor/charm.land/wish/v2/wish.go
+++ b/vendor/charm.land/wish/v2/wish.go
@@ -4,8 +4,8 @@ import (
"fmt"
"io"
+ "charm.land/ssh"
"github.com/charmbracelet/keygen"
- "github.com/charmbracelet/ssh"
)
// Middleware is a function that takes an ssh.Handler and returns an
@@ -37,7 +37,7 @@ func NewServer(ops ...ssh.Option) (*ssh.Server, error) {
}
// Fatal prints to the given session's STDERR and exits 1.
-func Fatal(s ssh.Session, v ...interface{}) {
+func Fatal(s ssh.Session, v ...any) {
Error(s, v...)
_ = s.Exit(1)
_ = s.Close()
@@ -47,7 +47,7 @@ func Fatal(s ssh.Session, v ...interface{}) {
// followed by an exit 1.
//
// Notice that this might cause formatting issues if you don't add a \r\n in the end of your string.
-func Fatalf(s ssh.Session, f string, v ...interface{}) {
+func Fatalf(s ssh.Session, f string, v ...any) {
Errorf(s, f, v...)
_ = s.Exit(1)
_ = s.Close()
@@ -55,7 +55,7 @@ func Fatalf(s ssh.Session, f string, v ...interface{}) {
// Fatalln formats according to the default format, prints to the session's
// STDERR, followed by a new line and an exit 1.
-func Fatalln(s ssh.Session, v ...interface{}) {
+func Fatalln(s ssh.Session, v ...any) {
Errorln(s, v...)
Errorf(s, "\r")
_ = s.Exit(1)
@@ -63,32 +63,32 @@ func Fatalln(s ssh.Session, v ...interface{}) {
}
// Error prints the given error the the session's STDERR.
-func Error(s ssh.Session, v ...interface{}) {
+func Error(s ssh.Session, v ...any) {
_, _ = fmt.Fprint(s.Stderr(), v...)
}
// Errorf formats according to the given format and prints to the session's STDERR.
-func Errorf(s ssh.Session, f string, v ...interface{}) {
+func Errorf(s ssh.Session, f string, v ...any) {
_, _ = fmt.Fprintf(s.Stderr(), f, v...)
}
// Errorln formats according to the default format and prints to the session's STDERR.
-func Errorln(s ssh.Session, v ...interface{}) {
+func Errorln(s ssh.Session, v ...any) {
_, _ = fmt.Fprintln(s.Stderr(), v...)
}
// Print writes to the session's STDOUT followed.
-func Print(s ssh.Session, v ...interface{}) {
+func Print(s ssh.Session, v ...any) {
_, _ = fmt.Fprint(s, v...)
}
// Printf formats according to the given format and writes to the session's STDOUT.
-func Printf(s ssh.Session, f string, v ...interface{}) {
+func Printf(s ssh.Session, f string, v ...any) {
_, _ = fmt.Fprintf(s, f, v...)
}
// Println formats according to the default format and writes to the session's STDOUT.
-func Println(s ssh.Session, v ...interface{}) {
+func Println(s ssh.Session, v ...any) {
_, _ = fmt.Fprintln(s, v...)
}
diff --git a/vendor/github.com/charmbracelet/x/conpty/conpty.go b/vendor/github.com/charmbracelet/x/conpty/conpty.go
index 7157b5f894..73e21349b6 100644
--- a/vendor/github.com/charmbracelet/x/conpty/conpty.go
+++ b/vendor/github.com/charmbracelet/x/conpty/conpty.go
@@ -1,17 +1,16 @@
package conpty
import (
- "os"
"syscall"
)
type pty interface {
Close() error
Fd() uintptr
- InPipe() *os.File
- InPipeFd() uintptr
- OutPipe() *os.File
- OutPipeFd() uintptr
+ InPipeReadFd() uintptr
+ InPipeWriteFd() uintptr
+ OutPipeReadFd() uintptr
+ OutPipeWriteFd() uintptr
Read(p []byte) (n int, err error)
Resize(w int, h int) error
Size() (w int, h int, err error)
diff --git a/vendor/github.com/charmbracelet/x/conpty/conpty_other.go b/vendor/github.com/charmbracelet/x/conpty/conpty_other.go
index 9ecc5f9b06..87ccf33737 100644
--- a/vendor/github.com/charmbracelet/x/conpty/conpty_other.go
+++ b/vendor/github.com/charmbracelet/x/conpty/conpty_other.go
@@ -4,7 +4,6 @@
package conpty
import (
- "os"
"syscall"
)
@@ -48,23 +47,23 @@ func (*ConPty) Resize(int, int) error {
return ErrUnsupported
}
-// InPipe implements pty.
-func (c *ConPty) InPipe() *os.File {
- return nil
+// InPipeReadFd returns the input pipe read file descriptor.
+func (*ConPty) InPipeReadFd() uintptr {
+ return 0
}
-// InPipeFd implements pty.
-func (c *ConPty) InPipeFd() uintptr {
+// InPipeWriteFd returns the input pipe write file descriptor.
+func (*ConPty) InPipeWriteFd() uintptr {
return 0
}
-// OutPipe implements pty.
-func (c *ConPty) OutPipe() *os.File {
- return nil
+// OutPipeReadFd returns the output pipe read file descriptor.
+func (*ConPty) OutPipeReadFd() uintptr {
+ return 0
}
-// OutPipeFd implements pty.
-func (c *ConPty) OutPipeFd() uintptr {
+// OutPipeWriteFd returns the output pipe write file descriptor.
+func (*ConPty) OutPipeWriteFd() uintptr {
return 0
}
diff --git a/vendor/github.com/charmbracelet/x/conpty/conpty_windows.go b/vendor/github.com/charmbracelet/x/conpty/conpty_windows.go
index 5efe300a6d..cfd00cdcfb 100644
--- a/vendor/github.com/charmbracelet/x/conpty/conpty_windows.go
+++ b/vendor/github.com/charmbracelet/x/conpty/conpty_windows.go
@@ -4,32 +4,25 @@
package conpty
import (
+ "errors"
"fmt"
"io"
- "os"
"sync"
"syscall"
"unsafe"
- "github.com/charmbracelet/x/errors"
"golang.org/x/sys/windows"
)
-// Default size.
-const (
- DefaultWidth = 80
- DefaultHeight = 25
-)
-
// ConPty represents a Windows Console Pseudo-terminal.
// https://learn.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session#preparing-the-communication-channels
type ConPty struct {
- hpc *windows.Handle
- inPipeFd, outPipeFd windows.Handle
- inPipe, outPipe *os.File
- attrList *windows.ProcThreadAttributeListContainer
- size windows.Coord
- closeOnce sync.Once
+ hpc *windows.Handle
+ inPipeWrite, inPipeRead windows.Handle
+ outPipeWrite, outPipeRead windows.Handle
+ attrList *windows.ProcThreadAttributeListContainer
+ size windows.Coord
+ closeOnce sync.Once
}
var (
@@ -37,10 +30,52 @@ var (
_ io.Reader = &ConPty{}
)
+// CreatePipes is a helper function to create connected input and output pipes.
+func CreatePipes() (inPipeRead, inPipeWrite, outPipeRead, outPipeWrite uintptr, err error) {
+ var inPipeReadHandle, inPipeWriteHandle windows.Handle
+ var outPipeReadHandle, outPipeWriteHandle windows.Handle
+ pSec := &windows.SecurityAttributes{Length: uint32(unsafe.Sizeof(zeroSec)), InheritHandle: 1}
+
+ if err := windows.CreatePipe(&inPipeReadHandle, &inPipeWriteHandle, pSec, 0); err != nil {
+ return 0, 0, 0, 0, fmt.Errorf("failed to create input pipes for pseudo console: %w", err)
+ }
+
+ if err := windows.CreatePipe(&outPipeReadHandle, &outPipeWriteHandle, pSec, 0); err != nil {
+ return 0, 0, 0, 0, fmt.Errorf("failed to create output pipes for pseudo console: %w", err)
+ }
+
+ return uintptr(inPipeReadHandle), uintptr(inPipeWriteHandle),
+ uintptr(outPipeReadHandle), uintptr(outPipeWriteHandle),
+ nil
+}
+
// New creates a new ConPty device.
// Accepts a custom width, height, and flags that will get passed to
// windows.CreatePseudoConsole.
-func New(w int, h int, flags int) (c *ConPty, err error) {
+func New(w int, h int, flags int) (*ConPty, error) {
+ inPipeRead, inPipeWrite, outPipeRead, outPipeWrite, err := CreatePipes()
+ if err != nil {
+ return nil, fmt.Errorf("failed to create pipes for pseudo console: %w", err)
+ }
+
+ c, err := NewWithPipes(inPipeRead, inPipeWrite, outPipeRead, outPipeWrite, w, h, flags)
+ if err != nil {
+ return nil, err
+ }
+
+ return c, nil
+}
+
+// NewWithPipes creates a new ConPty device with the provided pipe handles.
+// This is useful for when you want to use existing pipes, such as when
+// using a ConPty with a process that has already been created, or when
+// you want to use a ConPty with a specific set of pipes for input and output.
+//
+// The PTY-slave end (input read and output write) of the pipes can be closed
+// after the ConPty is created, as the ConPty will take ownership of the handles
+// and dup them for the new process that will be spawned. The PTY-master end of
+// the pipes will be used to communicate with the pseudo console.
+func NewWithPipes(inPipeRead, inPipeWrite, outPipeRead, outPipeWrite uintptr, w int, h int, flags int) (c *ConPty, err error) {
if w <= 0 {
w = DefaultWidth
}
@@ -53,33 +88,16 @@ func New(w int, h int, flags int) (c *ConPty, err error) {
size: windows.Coord{
X: int16(w), Y: int16(h),
},
+ inPipeWrite: windows.Handle(inPipeWrite),
+ inPipeRead: windows.Handle(inPipeRead),
+ outPipeWrite: windows.Handle(outPipeWrite),
+ outPipeRead: windows.Handle(outPipeRead),
}
- var ptyIn, ptyOut windows.Handle
- if err := windows.CreatePipe(&ptyIn, &c.inPipeFd, nil, 0); err != nil {
- return nil, fmt.Errorf("failed to create pipes for pseudo console: %w", err)
- }
-
- if err := windows.CreatePipe(&c.outPipeFd, &ptyOut, nil, 0); err != nil {
- return nil, fmt.Errorf("failed to create pipes for pseudo console: %w", err)
- }
-
- if err := windows.CreatePseudoConsole(c.size, ptyIn, ptyOut, uint32(flags), c.hpc); err != nil {
+ if err := windows.CreatePseudoConsole(c.size, windows.Handle(inPipeRead), windows.Handle(outPipeWrite), uint32(flags), c.hpc); err != nil {
return nil, fmt.Errorf("failed to create pseudo console: %w", err)
}
- // We don't need the pty pipes anymore, these will get dup'd when the
- // new process starts.
- if err := windows.CloseHandle(ptyOut); err != nil {
- return nil, fmt.Errorf("failed to close pseudo console handle: %w", err)
- }
- if err := windows.CloseHandle(ptyIn); err != nil {
- return nil, fmt.Errorf("failed to close pseudo console handle: %w", err)
- }
-
- c.inPipe = os.NewFile(uintptr(c.inPipeFd), "|0")
- c.outPipe = os.NewFile(uintptr(c.outPipeFd), "|1")
-
// Allocate an attribute list that's large enough to do the operations we care about
// 1. Pseudo console setup
c.attrList, err = windows.NewProcThreadAttributeList(1)
@@ -95,7 +113,7 @@ func New(w int, h int, flags int) (c *ConPty, err error) {
return nil, fmt.Errorf("failed to update proc thread attributes for pseudo console: %w", err)
}
- return
+ return c, err
}
// Fd returns the ConPty handle.
@@ -107,46 +125,52 @@ func (p *ConPty) Fd() uintptr {
func (p *ConPty) Close() error {
var err error
p.closeOnce.Do(func() {
+ // Ensure that we have the PTY-end of the pipes closed.
+ _ = windows.CloseHandle(p.inPipeRead)
+ _ = windows.CloseHandle(p.outPipeWrite)
if p.attrList != nil {
p.attrList.Delete()
}
windows.ClosePseudoConsole(*p.hpc)
- err = errors.Join(p.inPipe.Close(), p.outPipe.Close())
+ err = errors.Join(
+ windows.CloseHandle(p.inPipeWrite),
+ windows.CloseHandle(p.outPipeRead),
+ )
})
return err
}
-// InPipe returns the ConPty input pipe.
-func (p *ConPty) InPipe() *os.File {
- return p.inPipe
+// InPipeReadFd returns the ConPty input pipe read file descriptor handle.
+func (p *ConPty) InPipeReadFd() uintptr {
+ return uintptr(p.inPipeRead)
}
-// InPipeFd returns the ConPty input pipe file descriptor handle.
-func (p *ConPty) InPipeFd() uintptr {
- return uintptr(p.inPipeFd)
+// InPipeWriteFd returns the ConPty input pipe write file descriptor handle.
+func (p *ConPty) InPipeWriteFd() uintptr {
+ return uintptr(p.inPipeWrite)
}
-// OutPipe returns the ConPty output pipe.
-func (p *ConPty) OutPipe() *os.File {
- return p.outPipe
+// OutPipeReadFd returns the ConPty output pipe read file descriptor handle.
+func (p *ConPty) OutPipeReadFd() uintptr {
+ return uintptr(p.outPipeRead)
}
-// OutPipeFd returns the ConPty output pipe file descriptor handle.
-func (p *ConPty) OutPipeFd() uintptr {
- return uintptr(p.outPipeFd)
+// OutPipeWriteFd returns the ConPty output pipe write file descriptor handle.
+func (p *ConPty) OutPipeWriteFd() uintptr {
+ return uintptr(p.outPipeWrite)
}
-// Write safely writes bytes to the ConPty.
+// Write safely writes bytes to master end of the ConPty.
func (c *ConPty) Write(p []byte) (n int, err error) {
var l uint32
- err = windows.WriteFile(c.inPipeFd, p, &l, nil)
+ err = windows.WriteFile(c.inPipeWrite, p, &l, nil)
return int(l), err
}
-// Read safely reads bytes from the ConPty.
+// Read safely reads bytes from master end of the ConPty.
func (c *ConPty) Read(p []byte) (n int, err error) {
var l uint32
- err = windows.ReadFile(c.outPipeFd, p, &l, nil)
+ err = windows.ReadFile(c.outPipeRead, p, &l, nil)
return int(l), err
}
@@ -164,10 +188,13 @@ func (c *ConPty) Resize(w int, h int) error {
func (c *ConPty) Size() (w int, h int, err error) {
w = int(c.size.X)
h = int(c.size.Y)
- return
+ return w, h, err
}
-var zeroAttr syscall.ProcAttr
+var (
+ zeroAttr syscall.ProcAttr
+ zeroSec windows.SecurityAttributes
+)
// Spawn spawns a new process attached to the pseudo-console.
func (c *ConPty) Spawn(name string, args []string, attr *syscall.ProcAttr) (pid int, handle uintptr, err error) {
@@ -233,7 +260,6 @@ func (c *ConPty) Spawn(name string, args []string, attr *syscall.ProcAttr) (pid
flags |= attr.Sys.CreationFlags
}
- var zeroSec windows.SecurityAttributes
pSec := &windows.SecurityAttributes{Length: uint32(unsafe.Sizeof(zeroSec)), InheritHandle: 1}
if attr.Sys != nil && attr.Sys.ProcessAttributes != nil {
pSec = &windows.SecurityAttributes{
diff --git a/vendor/github.com/charmbracelet/x/conpty/doc.go b/vendor/github.com/charmbracelet/x/conpty/doc.go
index 461be34600..0f44949c43 100644
--- a/vendor/github.com/charmbracelet/x/conpty/doc.go
+++ b/vendor/github.com/charmbracelet/x/conpty/doc.go
@@ -7,3 +7,9 @@ import "errors"
// ErrUnsupported is returned when the current platform is not supported.
var ErrUnsupported = errors.New("conpty: unsupported platform")
+
+// Default size.
+const (
+ DefaultWidth = 80
+ DefaultHeight = 25
+)
diff --git a/vendor/github.com/charmbracelet/x/conpty/exec_windows.go b/vendor/github.com/charmbracelet/x/conpty/exec_windows.go
index bbbbaae9a3..3c40d2799f 100644
--- a/vendor/github.com/charmbracelet/x/conpty/exec_windows.go
+++ b/vendor/github.com/charmbracelet/x/conpty/exec_windows.go
@@ -80,7 +80,7 @@ func execEnvDefault(sys *syscall.SysProcAttr) (env []string, err error) {
env = append(env, string(utf16.Decode(entry)))
blockp += 2 * (uintptr(len(entry)) + 1)
}
- return
+ return env, err
}
func isSlash(c uint8) bool {
diff --git a/vendor/github.com/charmbracelet/x/errors/join.go b/vendor/github.com/charmbracelet/x/errors/join.go
deleted file mode 100644
index c97c420455..0000000000
--- a/vendor/github.com/charmbracelet/x/errors/join.go
+++ /dev/null
@@ -1,47 +0,0 @@
-// Package errors provides error handling utilities.
-package errors
-
-import "strings"
-
-// Join returns an error that wraps the given errors.
-// Any nil error values are discarded.
-// Join returns nil if every value in errs is nil.
-// The error formats as the concatenation of the strings obtained
-// by calling the Error method of each element of errs, with a newline
-// between each string.
-//
-// A non-nil error returned by Join implements the Unwrap() []error method.
-//
-// This is copied from Go 1.20 errors.Unwrap, with some tuning to avoid using unsafe.
-// The main goal is to have this available in older Go versions.
-func Join(errs ...error) error {
- var nonNil []error //nolint:prealloc
- for _, err := range errs {
- if err == nil {
- continue
- }
- nonNil = append(nonNil, err)
- }
- if len(nonNil) == 0 {
- return nil
- }
- return &joinError{
- errs: nonNil,
- }
-}
-
-type joinError struct {
- errs []error
-}
-
-func (e *joinError) Error() string {
- strs := make([]string, 0, len(e.errs))
- for _, err := range e.errs {
- strs = append(strs, err.Error())
- }
- return strings.Join(strs, "\n")
-}
-
-func (e *joinError) Unwrap() []error {
- return e.errs
-}
diff --git a/vendor/github.com/charmbracelet/x/errors/LICENSE b/vendor/github.com/charmbracelet/x/xpty/LICENSE
similarity index 100%
rename from vendor/github.com/charmbracelet/x/errors/LICENSE
rename to vendor/github.com/charmbracelet/x/xpty/LICENSE
diff --git a/vendor/github.com/charmbracelet/x/xpty/conpty.go b/vendor/github.com/charmbracelet/x/xpty/conpty.go
new file mode 100644
index 0000000000..5efd228413
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/xpty/conpty.go
@@ -0,0 +1,40 @@
+package xpty
+
+import (
+ "os/exec"
+
+ "github.com/charmbracelet/x/conpty"
+)
+
+// ConPty is a Windows console pty.
+type ConPty struct {
+ *conpty.ConPty
+}
+
+var _ Pty = &ConPty{}
+
+// NewConPty creates a new ConPty.
+func NewConPty(width, height int, opts ...PtyOption) (*ConPty, error) {
+ var opt Options
+ for _, o := range opts {
+ o(opt)
+ }
+
+ c, err := conpty.New(width, height, opt.Flags)
+ if err != nil {
+ return nil, err //nolint:wrapcheck
+ }
+
+ return &ConPty{c}, nil
+}
+
+// Name returns the name of the ConPty.
+func (c *ConPty) Name() string {
+ return "windows-pty"
+}
+
+// Start starts a command on the ConPty.
+// This is a wrapper around conpty.Spawn.
+func (c *ConPty) Start(cmd *exec.Cmd) error {
+ return c.start(cmd)
+}
diff --git a/vendor/github.com/charmbracelet/x/xpty/conpty_other.go b/vendor/github.com/charmbracelet/x/xpty/conpty_other.go
new file mode 100644
index 0000000000..3c3382506a
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/xpty/conpty_other.go
@@ -0,0 +1,10 @@
+//go:build !windows
+// +build !windows
+
+package xpty
+
+import "os/exec"
+
+func (c *ConPty) start(*exec.Cmd) error {
+ return ErrUnsupported
+}
diff --git a/vendor/github.com/charmbracelet/x/xpty/conpty_windows.go b/vendor/github.com/charmbracelet/x/xpty/conpty_windows.go
new file mode 100644
index 0000000000..c42e7334d4
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/xpty/conpty_windows.go
@@ -0,0 +1,37 @@
+//go:build windows
+// +build windows
+
+package xpty
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "syscall"
+
+ "golang.org/x/sys/windows"
+)
+
+func (c *ConPty) start(cmd *exec.Cmd) error {
+ pid, proc, err := c.Spawn(cmd.Path, cmd.Args, &syscall.ProcAttr{
+ Dir: cmd.Dir,
+ Env: cmd.Env,
+ Sys: cmd.SysProcAttr,
+ })
+ if err != nil {
+ return err //nolint:wrapcheck
+ }
+
+ cmd.Process, err = os.FindProcess(pid)
+ if err != nil {
+ // If we can't find the process via os.FindProcess, terminate the
+ // process as that's what we rely on for all further operations on the
+ // object.
+ if tErr := windows.TerminateProcess(windows.Handle(proc), 1); tErr != nil {
+ return fmt.Errorf("failed to terminate process after process not found: %w", tErr)
+ }
+ return fmt.Errorf("failed to find process after starting: %w", err)
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/charmbracelet/x/xpty/pty.go b/vendor/github.com/charmbracelet/x/xpty/pty.go
new file mode 100644
index 0000000000..5b94715bd1
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/xpty/pty.go
@@ -0,0 +1,129 @@
+package xpty
+
+import (
+ "os"
+ "os/exec"
+
+ "github.com/creack/pty"
+)
+
+// UnixPty represents a classic Unix PTY (pseudo-terminal).
+type UnixPty struct {
+ master, slave *os.File
+}
+
+var _ Pty = &UnixPty{}
+
+// NewUnixPty creates a new Unix PTY.
+func NewUnixPty(width, height int, _ ...PtyOption) (*UnixPty, error) {
+ ptm, pts, err := pty.Open()
+ if err != nil {
+ return nil, err //nolint:wrapcheck
+ }
+
+ p := &UnixPty{
+ master: ptm,
+ slave: pts,
+ }
+
+ if width >= 0 && height >= 0 {
+ if err := p.Resize(width, height); err != nil {
+ return nil, err
+ }
+ }
+
+ return p, nil
+}
+
+// Close implements XPTY.
+func (p *UnixPty) Close() (err error) {
+ defer func() {
+ serr := p.slave.Close()
+ if err == nil {
+ err = serr
+ }
+ }()
+ if err := p.master.Close(); err != nil {
+ return err //nolint:wrapcheck
+ }
+ return err
+}
+
+// Fd implements XPTY.
+func (p *UnixPty) Fd() uintptr {
+ return p.master.Fd()
+}
+
+// Name implements XPTY.
+func (p *UnixPty) Name() string {
+ return p.master.Name()
+}
+
+// SlaveName returns the name of the slave PTY.
+// This is usually used for remote sessions to identify the running TTY. You
+// can find this in SSH sessions defined as $SSH_TTY.
+func (p *UnixPty) SlaveName() string {
+ return p.slave.Name()
+}
+
+// Read implements XPTY.
+func (p *UnixPty) Read(b []byte) (n int, err error) {
+ return p.master.Read(b) //nolint:wrapcheck
+}
+
+// Resize implements XPTY.
+func (p *UnixPty) Resize(width int, height int) (err error) {
+ return p.setWinsize(width, height, 0, 0)
+}
+
+// SetWinsize sets window size for the PTY.
+func (p *UnixPty) SetWinsize(width, height, x, y int) error {
+ return p.setWinsize(width, height, x, y)
+}
+
+// Size returns the size of the PTY.
+func (p *UnixPty) Size() (width, height int, err error) {
+ return p.size()
+}
+
+// Start implements XPTY.
+func (p *UnixPty) Start(c *exec.Cmd) error {
+ if c.Stdout == nil {
+ c.Stdout = p.slave
+ }
+ if c.Stderr == nil {
+ c.Stderr = p.slave
+ }
+ if c.Stdin == nil {
+ c.Stdin = p.slave
+ }
+ if err := c.Start(); err != nil {
+ return err //nolint:wrapcheck
+ }
+ return nil
+}
+
+// Write implements XPTY.
+func (p *UnixPty) Write(b []byte) (n int, err error) {
+ return p.master.Write(b) //nolint:wrapcheck
+}
+
+// Master returns the master end of the PTY.
+func (p *UnixPty) Master() *os.File {
+ return p.master
+}
+
+// Slave returns the slave end of the PTY.
+func (p *UnixPty) Slave() *os.File {
+ return p.slave
+}
+
+// Control runs the given function with the file descriptor of the master PTY.
+func (p *UnixPty) Control(fn func(fd uintptr)) error {
+ conn, err := p.master.SyscallConn()
+ if err != nil {
+ return err //nolint:wrapcheck
+ }
+
+ return conn.Control(fn) //nolint:wrapcheck
+}
diff --git a/vendor/github.com/charmbracelet/x/xpty/pty_other.go b/vendor/github.com/charmbracelet/x/xpty/pty_other.go
new file mode 100644
index 0000000000..df2592c079
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/xpty/pty_other.go
@@ -0,0 +1,12 @@
+//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris
+// +build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris
+
+package xpty
+
+func (p *UnixPty) setWinsize(int, int, int, int) error {
+ return ErrUnsupported
+}
+
+func (*UnixPty) size() (int, int, error) {
+ return 0, 0, ErrUnsupported
+}
diff --git a/vendor/github.com/charmbracelet/x/xpty/pty_unix.go b/vendor/github.com/charmbracelet/x/xpty/pty_unix.go
new file mode 100644
index 0000000000..5cfa0696b3
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/xpty/pty_unix.go
@@ -0,0 +1,43 @@
+//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
+// +build darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package xpty
+
+import (
+ "github.com/charmbracelet/x/termios"
+ "golang.org/x/sys/unix"
+)
+
+// setWinsize sets window size for the PTY.
+func (p *UnixPty) setWinsize(width, height, x, y int) error {
+ var rErr error
+ if err := p.Control(func(fd uintptr) {
+ rErr = termios.SetWinsize(int(fd), &unix.Winsize{
+ Row: uint16(height), //nolint:gosec
+ Col: uint16(width), //nolint:gosec
+ Xpixel: uint16(x), //nolint:gosec
+ Ypixel: uint16(y), //nolint:gosec
+ })
+ }); err != nil {
+ rErr = err
+ }
+ return rErr
+}
+
+// size returns the size of the PTY.
+func (p *UnixPty) size() (width, height int, err error) {
+ var rErr error
+ if err := p.Control(func(fd uintptr) {
+ ws, err := termios.GetWinsize(int(fd))
+ if err != nil {
+ rErr = err
+ return
+ }
+ width = int(ws.Col)
+ height = int(ws.Row)
+ }); err != nil {
+ rErr = err
+ }
+
+ return width, height, rErr
+}
diff --git a/vendor/github.com/charmbracelet/x/xpty/xpty.go b/vendor/github.com/charmbracelet/x/xpty/xpty.go
new file mode 100644
index 0000000000..e9e5e1c7d7
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/xpty/xpty.go
@@ -0,0 +1,128 @@
+// Package xpty provides platform-independent interfaces to interact with
+// pseudo-terminals (PTYs) in Go. It abstracts the differences between Unix and
+// Windows systems and supports both ConPTY and classic Unix PTYs.
+package xpty
+
+import (
+ "context"
+ "errors"
+ "io"
+ "os"
+ "os/exec"
+ "runtime"
+
+ "github.com/charmbracelet/x/term"
+ "github.com/creack/pty"
+)
+
+// ErrUnsupported is returned when a feature is not supported.
+var ErrUnsupported = pty.ErrUnsupported
+
+// Pty represents a PTY (pseudo-terminal) interface.
+type Pty interface {
+ term.File
+ io.ReadWriteCloser
+
+ // Resize resizes the PTY.
+ Resize(width, height int) error
+
+ // Size returns the size of the PTY.
+ Size() (width, height int, err error)
+
+ // Name returns the name of the PTY.
+ Name() string
+
+ // Start starts a command on the PTY.
+ // The command started will have its standard input, output, and error
+ // connected to the PTY.
+ // On Windows, calling Wait won't work since the Go runtime doesn't handle
+ // ConPTY processes correctly. See https://github.com/golang/go/pull/62710.
+ Start(cmd *exec.Cmd) error
+}
+
+// Options represents PTY options.
+type Options struct {
+ Flags int
+}
+
+// PtyOption is a PTY option.
+type PtyOption func(o Options)
+
+// NewPty creates a new PTY.
+//
+// The returned PTY will be a Unix PTY on Unix systems and a ConPTY on Windows.
+// The width and height parameters specify the initial size of the PTY.
+// You can pass additional options to the PTY by passing PtyOptions.
+//
+// pty, err := xpty.NewPty(80, 24)
+// if err != nil {
+// // handle error
+// }
+//
+// defer pty.Close() // Make sure to close the PTY when done.
+// switch pty := pty.(type) {
+// case xpty.UnixPty:
+// // Unix PTY
+// case xpty.ConPty:
+// // ConPTY
+// }
+func NewPty(width, height int, opts ...PtyOption) (Pty, error) {
+ if runtime.GOOS == "windows" {
+ return NewConPty(width, height, opts...)
+ }
+ return NewUnixPty(width, height, opts...)
+}
+
+// WaitProcess waits for the process to exit.
+// This exists because on Windows, cmd.Wait() doesn't work with ConPty.
+// When the OS is not windows, it'll simply fall back to cmd.Wait().
+//
+// If ctx is canceled, the process is killed, WaitProcess waits for it to be
+// reaped, and the returned error reports the cancellation: ctx.Err() on a
+// successful kill, or an error joining ctx.Err() and the kill error if the
+// kill itself failed.
+func WaitProcess(ctx context.Context, cmd *exec.Cmd) (err error) {
+ if runtime.GOOS != "windows" {
+ return cmd.Wait() //nolint:wrapcheck
+ }
+
+ if cmd.Process == nil {
+ return errors.New("process not started")
+ }
+
+ type result struct {
+ *os.ProcessState
+ error
+ }
+
+ donec := make(chan result, 1)
+ go func() {
+ state, err := cmd.Process.Wait()
+ donec <- result{state, err}
+ }()
+
+ select {
+ case <-ctx.Done():
+ killErr := cmd.Process.Kill()
+ // Reap the process so it doesn't linger as a zombie.
+ r := <-donec
+ cmd.ProcessState = r.ProcessState
+ if killErr != nil {
+ return errors.Join(ctx.Err(), killErr)
+ }
+ return ctx.Err() //nolint:wrapcheck // sentinel; callers use errors.Is
+ case r := <-donec:
+ cmd.ProcessState = r.ProcessState
+ err = r.error
+ }
+
+ // On Windows, os.Process.Wait returns nil even when the process exits
+ // with a non-zero code; the *exec.ExitError that exec.Cmd.Wait would
+ // return never materializes. Synthesize it so callers get the same
+ // error shape as exec.Cmd.Wait on any platform.
+ if err == nil && cmd.ProcessState != nil && !cmd.ProcessState.Success() {
+ err = &exec.ExitError{ProcessState: cmd.ProcessState}
+ }
+
+ return err
+}
diff --git a/vendor/github.com/gokrazy/updater/updater.go b/vendor/github.com/gokrazy/updater/updater.go
index 9be0d04d47..2221c39363 100644
--- a/vendor/github.com/gokrazy/updater/updater.go
+++ b/vendor/github.com/gokrazy/updater/updater.go
@@ -16,7 +16,6 @@ import (
"log"
"net/http"
"net/url"
- "slices"
"strings"
)
@@ -76,7 +75,12 @@ const (
// Supports returns whether the target is known to support the specified update
// protocol feature.
func (t *Target) Supports(feature ProtocolFeature) bool {
- return slices.Contains(t.supports, string(feature))
+ for _, f := range t.supports {
+ if f == string(feature) {
+ return true
+ }
+ }
+ return false
}
// StreamTo streams from the specified io.Reader to the specified destination:
@@ -118,7 +122,7 @@ func (t *Target) StreamTo(ctx context.Context, dest string, r io.Reader) error {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("unexpected HTTP status code: got %v, want %v (body %q)", resp.Status, want, string(body))
}
- remoteHash, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
+ remoteHash, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
@@ -162,7 +166,7 @@ func (t *Target) Put(ctx context.Context, dest string, r io.Reader) error {
// Switch changes the active root partition from the currently running root
// partition to the currently inactive root partition.
func (t *Target) Switch(ctx context.Context) error {
- req, err := http.NewRequestWithContext(ctx, "POST", t.baseURL+"update/switch", http.NoBody)
+ req, err := http.NewRequestWithContext(ctx, "POST", t.baseURL+"update/switch", nil)
if err != nil {
return err
}
@@ -181,7 +185,7 @@ func (t *Target) Switch(ctx context.Context) error {
// Testboot marks the inactive root partition to be tested upon the next boot,
// and made active if the test boot succeeds.
func (t *Target) Testboot(ctx context.Context) error {
- req, err := http.NewRequestWithContext(ctx, "POST", t.baseURL+"update/testboot", http.NoBody)
+ req, err := http.NewRequestWithContext(ctx, "POST", t.baseURL+"update/testboot", nil)
if err != nil {
return err
}
@@ -245,7 +249,7 @@ func (t *Target) Reboot(ctx context.Context, opts ...RebootOption) error {
url += "?" + strings.Join(params, "&")
}
- req, err := http.NewRequestWithContext(ctx, "POST", url, http.NoBody)
+ req, err := http.NewRequestWithContext(ctx, "POST", url, nil)
if err != nil {
return err
}
@@ -335,7 +339,7 @@ func (t *Target) InstalledEEPROM() EEPROMVersion {
}
func (t *Target) requestFeatures(ctx context.Context) error {
- req, err := http.NewRequestWithContext(ctx, "GET", t.baseURL+"update/features", http.NoBody)
+ req, err := http.NewRequestWithContext(ctx, "GET", t.baseURL+"update/features", nil)
if err != nil {
return err
}
@@ -357,7 +361,7 @@ func (t *Target) requestFeatures(ctx context.Context) error {
return fmt.Errorf("unexpected HTTP status code: got %d, want %d (body %q)", got, want, string(body))
}
- body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
+ body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
@@ -399,7 +403,7 @@ type EEPROMVersion struct {
}
func (t *Target) getEEPROMFromStatus(ctx context.Context) (*EEPROMVersion, error) {
- req, err := http.NewRequestWithContext(ctx, "GET", t.baseURL, http.NoBody)
+ req, err := http.NewRequestWithContext(ctx, "GET", t.baseURL, nil)
if err != nil {
return nil, err
}
diff --git a/vendor/github.com/pires/go-proxyproto/.gitignore b/vendor/github.com/pires/go-proxyproto/.gitignore
new file mode 100644
index 0000000000..a2d2c30197
--- /dev/null
+++ b/vendor/github.com/pires/go-proxyproto/.gitignore
@@ -0,0 +1,11 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+.idea
+bin
+pkg
+
+*.out
diff --git a/vendor/github.com/pires/go-proxyproto/.golangci.yml b/vendor/github.com/pires/go-proxyproto/.golangci.yml
new file mode 100644
index 0000000000..290eff06fa
--- /dev/null
+++ b/vendor/github.com/pires/go-proxyproto/.golangci.yml
@@ -0,0 +1,27 @@
+version: "2"
+
+linters:
+ default: standard
+ enable:
+ - asasalint
+ - asciicheck
+ - bidichk
+ - bodyclose
+ - canonicalheader
+ - containedctx
+ - copyloopvar
+ - goconst
+ - godot
+ - gosec
+ - modernize
+ - misspell
+ - revive
+ - unconvert
+ - usestdlibvars
+
+run:
+ timeout: 5m
+ allow-parallel-runners: true
+
+issues:
+ max-issues-per-linter: 0
diff --git a/vendor/github.com/pires/go-proxyproto/LICENSE b/vendor/github.com/pires/go-proxyproto/LICENSE
new file mode 100644
index 0000000000..a65c05a627
--- /dev/null
+++ b/vendor/github.com/pires/go-proxyproto/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2016 Paulo Pires
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/vendor/github.com/pires/go-proxyproto/README.md b/vendor/github.com/pires/go-proxyproto/README.md
new file mode 100644
index 0000000000..2cbc941fed
--- /dev/null
+++ b/vendor/github.com/pires/go-proxyproto/README.md
@@ -0,0 +1,162 @@
+# go-proxyproto
+
+[](https://github.com/pires/go-proxyproto/actions)
+[](https://coveralls.io/github/pires/go-proxyproto?branch=main)
+[](https://goreportcard.com/report/github.com/pires/go-proxyproto)
+[](https://pkg.go.dev/github.com/pires/go-proxyproto?tab=doc)
+
+
+A Go library implementation of the [PROXY protocol, versions 1 and 2](https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt),
+which provides, as per specification:
+> (...) a convenient way to safely transport connection
+> information such as a client's address across multiple layers of NAT or TCP
+> proxies. It is designed to require little changes to existing components and
+> to limit the performance impact caused by the processing of the transported
+> information.
+
+This library is to be used in one of or both proxy clients and proxy servers that need to support said protocol.
+Both protocol versions, 1 (text-based) and 2 (binary-based) are supported.
+
+## Installation
+
+```shell
+$ go get -u github.com/pires/go-proxyproto
+```
+
+## Usage
+
+### Client
+
+```go
+package main
+
+import (
+ "io"
+ "log"
+ "net"
+
+ proxyproto "github.com/pires/go-proxyproto"
+)
+
+func chkErr(err error) {
+ if err != nil {
+ log.Fatalf("Error: %s", err.Error())
+ }
+}
+
+func main() {
+ // Dial some proxy listener e.g. https://github.com/mailgun/proxyproto
+ target, err := net.ResolveTCPAddr("tcp", "127.0.0.1:2319")
+ chkErr(err)
+
+ conn, err := net.DialTCP("tcp", nil, target)
+ chkErr(err)
+
+ defer conn.Close()
+
+ // Create a proxyprotocol header or use HeaderProxyFromAddrs() if you
+ // have two conn's
+ header := &proxyproto.Header{
+ Version: 1,
+ Command: proxyproto.PROXY,
+ TransportProtocol: proxyproto.TCPv4,
+ SourceAddr: &net.TCPAddr{
+ IP: net.ParseIP("10.1.1.1"),
+ Port: 1000,
+ },
+ DestinationAddr: &net.TCPAddr{
+ IP: net.ParseIP("20.2.2.2"),
+ Port: 2000,
+ },
+ }
+ // After the connection was created write the proxy headers first
+ _, err = header.WriteTo(conn)
+ chkErr(err)
+ // Then your data... e.g.:
+ _, err = io.WriteString(conn, "HELO")
+ chkErr(err)
+}
+```
+
+### Server
+
+```go
+package main
+
+import (
+ "log"
+ "net"
+
+ proxyproto "github.com/pires/go-proxyproto"
+)
+
+func main() {
+ // Create a listener
+ addr := "localhost:9876"
+ list, err := net.Listen("tcp", addr)
+ if err != nil {
+ log.Fatalf("couldn't listen to %q: %q\n", addr, err.Error())
+ }
+
+ // Wrap listener in a proxyproto listener
+ proxyListener := &proxyproto.Listener{Listener: list}
+ defer proxyListener.Close()
+
+ // Wait for a connection and accept it
+ conn, err := proxyListener.Accept()
+ defer conn.Close()
+
+ // Print connection details
+ if conn.LocalAddr() == nil {
+ log.Fatal("couldn't retrieve local address")
+ }
+ log.Printf("local address: %q", conn.LocalAddr().String())
+
+ if conn.RemoteAddr() == nil {
+ log.Fatal("couldn't retrieve remote address")
+ }
+ log.Printf("remote address: %q", conn.RemoteAddr().String())
+}
+```
+
+### HTTP Server
+```go
+package main
+
+import (
+ "net"
+ "net/http"
+ "time"
+
+ "github.com/pires/go-proxyproto"
+)
+
+func main() {
+ server := http.Server{
+ Addr: ":8080",
+ }
+
+ ln, err := net.Listen("tcp", server.Addr)
+ if err != nil {
+ panic(err)
+ }
+
+ proxyListener := &proxyproto.Listener{
+ Listener: ln,
+ ReadHeaderTimeout: 10 * time.Second,
+ }
+ defer proxyListener.Close()
+
+ server.Serve(proxyListener)
+}
+```
+
+## Special notes
+
+### AWS
+
+AWS Network Load Balancer (NLB) does not push the PPV2 header until the client starts sending the data. This is a problem if your server speaks first. e.g. SMTP, FTP, SSH etc.
+
+By default, NLB target group attribute `proxy_protocol_v2.client_to_server.header_placement` has the value `on_first_ack_with_payload`. You need to contact AWS support to change it to `on_first_ack`, instead.
+
+Just to be clear, you need this fix only if your server is designed to speak first.
diff --git a/vendor/github.com/pires/go-proxyproto/addr_proto.go b/vendor/github.com/pires/go-proxyproto/addr_proto.go
new file mode 100644
index 0000000000..ec5b940870
--- /dev/null
+++ b/vendor/github.com/pires/go-proxyproto/addr_proto.go
@@ -0,0 +1,63 @@
+package proxyproto
+
+// AddressFamilyAndProtocol represents address family and transport protocol.
+type AddressFamilyAndProtocol byte
+
+// AddressFamilyAndProtocol enum values.
+const (
+ UNSPEC AddressFamilyAndProtocol = '\x00'
+ TCPv4 AddressFamilyAndProtocol = '\x11'
+ UDPv4 AddressFamilyAndProtocol = '\x12'
+ TCPv6 AddressFamilyAndProtocol = '\x21'
+ UDPv6 AddressFamilyAndProtocol = '\x22'
+ UnixStream AddressFamilyAndProtocol = '\x31'
+ UnixDatagram AddressFamilyAndProtocol = '\x32'
+)
+
+// IsIPv4 returns true if the address family is IPv4 (AF_INET4), false otherwise.
+func (ap AddressFamilyAndProtocol) IsIPv4() bool {
+ return ap&0xF0 == 0x10
+}
+
+// IsIPv6 returns true if the address family is IPv6 (AF_INET6), false otherwise.
+func (ap AddressFamilyAndProtocol) IsIPv6() bool {
+ return ap&0xF0 == 0x20
+}
+
+// IsUnix returns true if the address family is UNIX (AF_UNIX), false otherwise.
+func (ap AddressFamilyAndProtocol) IsUnix() bool {
+ return ap&0xF0 == 0x30
+}
+
+// IsStream returns true if the transport protocol is TCP or STREAM (SOCK_STREAM), false otherwise.
+func (ap AddressFamilyAndProtocol) IsStream() bool {
+ return ap&0x0F == 0x01
+}
+
+// IsDatagram returns true if the transport protocol is UDP or DGRAM (SOCK_DGRAM), false otherwise.
+func (ap AddressFamilyAndProtocol) IsDatagram() bool {
+ return ap&0x0F == 0x02
+}
+
+// IsUnspec returns true if the transport protocol or address family is unspecified, false otherwise.
+func (ap AddressFamilyAndProtocol) IsUnspec() bool {
+ return (ap&0xF0 == 0x00) || (ap&0x0F == 0x00)
+}
+
+func (ap AddressFamilyAndProtocol) toByte() byte {
+ if ap.IsIPv4() && ap.IsStream() {
+ return byte(TCPv4)
+ } else if ap.IsIPv4() && ap.IsDatagram() {
+ return byte(UDPv4)
+ } else if ap.IsIPv6() && ap.IsStream() {
+ return byte(TCPv6)
+ } else if ap.IsIPv6() && ap.IsDatagram() {
+ return byte(UDPv6)
+ } else if ap.IsUnix() && ap.IsStream() {
+ return byte(UnixStream)
+ } else if ap.IsUnix() && ap.IsDatagram() {
+ return byte(UnixDatagram)
+ }
+
+ return byte(UNSPEC)
+}
diff --git a/vendor/github.com/pires/go-proxyproto/header.go b/vendor/github.com/pires/go-proxyproto/header.go
new file mode 100644
index 0000000000..b1a6c33e1d
--- /dev/null
+++ b/vendor/github.com/pires/go-proxyproto/header.go
@@ -0,0 +1,302 @@
+// Package proxyproto implements Proxy Protocol (v1 and v2) parser and writer, as per specification:
+// https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt
+package proxyproto
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "io"
+ "net"
+ "time"
+)
+
+var (
+ // SIGV1 is the signature for PROXY protocol v1.
+ SIGV1 = []byte{'\x50', '\x52', '\x4F', '\x58', '\x59'}
+ // SIGV2 is the signature for PROXY protocol v2.
+ SIGV2 = []byte{'\x0D', '\x0A', '\x0D', '\x0A', '\x00', '\x0D', '\x0A', '\x51', '\x55', '\x49', '\x54', '\x0A'}
+
+ // ErrCantReadVersion1Header indicates a v1 header could not be read.
+ ErrCantReadVersion1Header = errors.New("proxyproto: can't read version 1 header")
+ // ErrVersion1HeaderTooLong indicates a v1 header is too long.
+ ErrVersion1HeaderTooLong = errors.New("proxyproto: version 1 header must be 107 bytes or less")
+ // ErrLineMustEndWithCrlf indicates a v1 header is invalid, must end with \r\n.
+ ErrLineMustEndWithCrlf = errors.New("proxyproto: version 1 header is invalid, must end with \\r\\n")
+ // ErrCantReadProtocolVersionAndCommand indicates a protocol version and command could not be read.
+ ErrCantReadProtocolVersionAndCommand = errors.New("proxyproto: can't read proxy protocol version and command")
+ // ErrCantReadAddressFamilyAndProtocol indicates an address family and protocol could not be read.
+ ErrCantReadAddressFamilyAndProtocol = errors.New("proxyproto: can't read address family or protocol")
+ // ErrCantReadLength indicates a length could not be read.
+ ErrCantReadLength = errors.New("proxyproto: can't read length")
+ // ErrCantResolveSourceUnixAddress indicates a source Unix address could not be resolved.
+ ErrCantResolveSourceUnixAddress = errors.New("proxyproto: can't resolve source Unix address")
+ // ErrCantResolveDestinationUnixAddress indicates a destination Unix address could not be resolved.
+ ErrCantResolveDestinationUnixAddress = errors.New("proxyproto: can't resolve destination Unix address")
+ // ErrNoProxyProtocol indicates a proxy protocol signature is not present.
+ ErrNoProxyProtocol = errors.New("proxyproto: proxy protocol signature not present")
+ // ErrUnknownProxyProtocolVersion indicates an unknown proxy protocol version.
+ ErrUnknownProxyProtocolVersion = errors.New("proxyproto: unknown proxy protocol version")
+ // ErrUnsupportedProtocolVersionAndCommand indicates an unsupported protocol version and command.
+ ErrUnsupportedProtocolVersionAndCommand = errors.New("proxyproto: unsupported proxy protocol version and command")
+ // ErrUnsupportedAddressFamilyAndProtocol indicates an unsupported address family and protocol.
+ ErrUnsupportedAddressFamilyAndProtocol = errors.New("proxyproto: unsupported address family and protocol")
+ // ErrInvalidLength indicates an invalid length.
+ ErrInvalidLength = errors.New("proxyproto: invalid length")
+ // ErrInvalidAddress indicates an invalid address.
+ ErrInvalidAddress = errors.New("proxyproto: invalid address")
+ // ErrInvalidPortNumber indicates an invalid port number.
+ ErrInvalidPortNumber = errors.New("proxyproto: invalid port number")
+ // ErrSuperfluousProxyHeader indicates an upstream connection sent a PROXY header but isn't allowed to send one.
+ ErrSuperfluousProxyHeader = errors.New("proxyproto: upstream connection sent PROXY header but isn't allowed to send one")
+)
+
+// Header is the placeholder for proxy protocol header.
+type Header struct {
+ Version byte
+ Command ProtocolVersionAndCommand
+ TransportProtocol AddressFamilyAndProtocol
+ SourceAddr net.Addr
+ DestinationAddr net.Addr
+ rawTLVs []byte
+}
+
+// HeaderProxyFromAddrs creates a new PROXY header from a source and a
+// destination address. If version is zero, the latest protocol version is
+// used.
+//
+// The header is filled on a best-effort basis: if hints cannot be inferred
+// from the provided addresses, the header will be left unspecified.
+func HeaderProxyFromAddrs(version byte, sourceAddr, destAddr net.Addr) *Header {
+ if version < 1 || version > 2 {
+ version = 2
+ }
+ h := &Header{
+ Version: version,
+ Command: LOCAL,
+ TransportProtocol: UNSPEC,
+ }
+ switch sourceAddr := sourceAddr.(type) {
+ case *net.TCPAddr:
+ if _, ok := destAddr.(*net.TCPAddr); !ok {
+ break
+ }
+ if len(sourceAddr.IP.To4()) == net.IPv4len {
+ h.TransportProtocol = TCPv4
+ } else if len(sourceAddr.IP) == net.IPv6len {
+ h.TransportProtocol = TCPv6
+ }
+ case *net.UDPAddr:
+ if _, ok := destAddr.(*net.UDPAddr); !ok {
+ break
+ }
+ if len(sourceAddr.IP.To4()) == net.IPv4len {
+ h.TransportProtocol = UDPv4
+ } else if len(sourceAddr.IP) == net.IPv6len {
+ h.TransportProtocol = UDPv6
+ }
+ case *net.UnixAddr:
+ if _, ok := destAddr.(*net.UnixAddr); !ok {
+ break
+ }
+ switch sourceAddr.Net {
+ case "unix":
+ h.TransportProtocol = UnixStream
+ case "unixgram":
+ h.TransportProtocol = UnixDatagram
+ }
+ }
+ if h.TransportProtocol != UNSPEC {
+ h.Command = PROXY
+ h.SourceAddr = sourceAddr
+ h.DestinationAddr = destAddr
+ }
+ return h
+}
+
+// TCPAddrs returns TCP source/destination addresses if the header is stream-based.
+func (header *Header) TCPAddrs() (sourceAddr, destAddr *net.TCPAddr, ok bool) {
+ if !header.TransportProtocol.IsStream() {
+ return nil, nil, false
+ }
+ sourceAddr, sourceOK := header.SourceAddr.(*net.TCPAddr)
+ destAddr, destOK := header.DestinationAddr.(*net.TCPAddr)
+ return sourceAddr, destAddr, sourceOK && destOK
+}
+
+// UDPAddrs returns UDP source/destination addresses if the header is datagram-based.
+func (header *Header) UDPAddrs() (sourceAddr, destAddr *net.UDPAddr, ok bool) {
+ if !header.TransportProtocol.IsDatagram() {
+ return nil, nil, false
+ }
+ sourceAddr, sourceOK := header.SourceAddr.(*net.UDPAddr)
+ destAddr, destOK := header.DestinationAddr.(*net.UDPAddr)
+ return sourceAddr, destAddr, sourceOK && destOK
+}
+
+// UnixAddrs returns UNIX source/destination addresses if the header is UNIX-based.
+func (header *Header) UnixAddrs() (sourceAddr, destAddr *net.UnixAddr, ok bool) {
+ if !header.TransportProtocol.IsUnix() {
+ return nil, nil, false
+ }
+ sourceAddr, sourceOK := header.SourceAddr.(*net.UnixAddr)
+ destAddr, destOK := header.DestinationAddr.(*net.UnixAddr)
+ return sourceAddr, destAddr, sourceOK && destOK
+}
+
+// IPs returns source/destination IPs for TCP/UDP headers.
+func (header *Header) IPs() (sourceIP, destIP net.IP, ok bool) {
+ if sourceAddr, destAddr, ok := header.TCPAddrs(); ok {
+ return sourceAddr.IP, destAddr.IP, true
+ }
+ if sourceAddr, destAddr, ok := header.UDPAddrs(); ok {
+ return sourceAddr.IP, destAddr.IP, true
+ }
+ return nil, nil, false
+}
+
+// Ports returns source/destination ports for TCP/UDP headers.
+func (header *Header) Ports() (sourcePort, destPort int, ok bool) {
+ if sourceAddr, destAddr, ok := header.TCPAddrs(); ok {
+ return sourceAddr.Port, destAddr.Port, true
+ }
+ if sourceAddr, destAddr, ok := header.UDPAddrs(); ok {
+ return sourceAddr.Port, destAddr.Port, true
+ }
+ return 0, 0, false
+}
+
+// EqualTo returns true if headers are equivalent, false otherwise.
+// Deprecated: use EqualsTo instead. This method will eventually be removed.
+func (header *Header) EqualTo(otherHeader *Header) bool {
+ return header.EqualsTo(otherHeader)
+}
+
+// EqualsTo returns true if headers are equivalent, false otherwise.
+func (header *Header) EqualsTo(otherHeader *Header) bool {
+ if otherHeader == nil {
+ return false
+ }
+ if header.Version != otherHeader.Version || header.Command != otherHeader.Command || header.TransportProtocol != otherHeader.TransportProtocol {
+ return false
+ }
+ // TLVs only exist for version 2
+ if header.Version == 2 && !bytes.Equal(header.rawTLVs, otherHeader.rawTLVs) {
+ return false
+ }
+ // Return early for header with LOCAL command, which contains no address information
+ if header.Command == LOCAL {
+ return true
+ }
+ return header.SourceAddr.String() == otherHeader.SourceAddr.String() &&
+ header.DestinationAddr.String() == otherHeader.DestinationAddr.String()
+}
+
+// WriteTo renders a proxy protocol header in a format and writes it to an io.Writer.
+func (header *Header) WriteTo(w io.Writer) (int64, error) {
+ buf, err := header.Format()
+ if err != nil {
+ return 0, err
+ }
+
+ return bytes.NewBuffer(buf).WriteTo(w)
+}
+
+// Format renders a proxy protocol header in a format to write over the wire.
+func (header *Header) Format() ([]byte, error) {
+ switch header.Version {
+ case 1:
+ return header.formatVersion1()
+ case 2:
+ return header.formatVersion2()
+ default:
+ return nil, ErrUnknownProxyProtocolVersion
+ }
+}
+
+// TLVs returns the TLVs stored into this header, if they exist. TLVs are optional for v2 of the protocol.
+func (header *Header) TLVs() ([]TLV, error) {
+ return SplitTLVs(header.rawTLVs)
+}
+
+// SetTLVs sets the TLVs stored in this header. This method replaces any
+// previous TLV.
+func (header *Header) SetTLVs(tlvs []TLV) error {
+ raw, err := JoinTLVs(tlvs)
+ if err != nil {
+ return err
+ }
+ header.rawTLVs = raw
+ return nil
+}
+
+// Read identifies the proxy protocol version and reads the remaining of
+// the header, accordingly.
+//
+// If proxy protocol header signature is not present, the reader buffer remains untouched
+// and is safe for reading outside of this code.
+//
+// If proxy protocol header signature is present but an error is raised while processing
+// the remaining header, assume the reader buffer to be in a corrupt state.
+// Also, this operation will block until enough bytes are available for peeking.
+func Read(reader *bufio.Reader) (*Header, error) {
+ // In order to improve speed for small non-PROXYed packets, take a peek at the first byte alone.
+ b1, err := reader.Peek(1)
+ if err != nil {
+ if err == io.EOF {
+ return nil, ErrNoProxyProtocol
+ }
+ return nil, err
+ }
+
+ if bytes.Equal(b1[:1], SIGV1[:1]) || bytes.Equal(b1[:1], SIGV2[:1]) {
+ signature, err := reader.Peek(5)
+ if err != nil {
+ if err == io.EOF {
+ return nil, ErrNoProxyProtocol
+ }
+ return nil, err
+ }
+ if bytes.Equal(signature[:5], SIGV1) {
+ return parseVersion1(reader)
+ }
+
+ signature, err = reader.Peek(12)
+ if err != nil {
+ if err == io.EOF {
+ return nil, ErrNoProxyProtocol
+ }
+ return nil, err
+ }
+ if bytes.Equal(signature[:12], SIGV2) {
+ return parseVersion2(reader)
+ }
+ }
+
+ return nil, ErrNoProxyProtocol
+}
+
+// ReadTimeout acts as Read but takes a timeout. If that timeout is reached, it's assumed
+// there's no proxy protocol header.
+func ReadTimeout(reader *bufio.Reader, timeout time.Duration) (*Header, error) {
+ type header struct {
+ h *Header
+ e error
+ }
+ read := make(chan *header, 1)
+
+ go func() {
+ h := &header{}
+ h.h, h.e = Read(reader)
+ read <- h
+ }()
+
+ timer := time.NewTimer(timeout)
+ select {
+ case result := <-read:
+ timer.Stop()
+ return result.h, result.e
+ case <-timer.C:
+ return nil, ErrNoProxyProtocol
+ }
+}
diff --git a/vendor/github.com/pires/go-proxyproto/policy.go b/vendor/github.com/pires/go-proxyproto/policy.go
new file mode 100644
index 0000000000..2124e46f2b
--- /dev/null
+++ b/vendor/github.com/pires/go-proxyproto/policy.go
@@ -0,0 +1,302 @@
+package proxyproto
+
+import (
+ "fmt"
+ "net"
+ "strings"
+)
+
+// PolicyFunc can be used to decide whether to trust the PROXY info from
+// upstream. If set, the connecting address is passed in as an argument.
+//
+// See below for the different policies.
+//
+// In case an error is returned the connection is denied.
+//
+// Deprecated: use ConnPolicyFunc instead.
+type PolicyFunc func(upstream net.Addr) (Policy, error)
+
+// ConnPolicyFunc can be used to decide whether to trust the PROXY info
+// based on connection policy options. If set, the connecting addresses
+// (remote and local) are passed in as argument.
+//
+// See below for the different policies.
+//
+// In case an error is returned the connection is denied.
+type ConnPolicyFunc func(connPolicyOptions ConnPolicyOptions) (Policy, error)
+
+// ConnPolicyOptions contains the remote and local addresses of a connection.
+type ConnPolicyOptions struct {
+ Upstream net.Addr
+ Downstream net.Addr
+}
+
+// Policy defines how a connection with a PROXY header address is treated.
+type Policy int
+
+const (
+ // USE address from PROXY header.
+ USE Policy = iota
+ // IGNORE address from PROXY header, but accept connection.
+ IGNORE
+ // REJECT connection when PROXY header is sent
+ // Note: even though the first read on the connection returns an error if
+ // a PROXY header is present, subsequent reads do not. It is the task of
+ // the code using the connection to handle that case properly.
+ REJECT
+ // REQUIRE connection to send PROXY header, reject if not present
+ // Note: even though the first read on the connection returns an error if
+ // a PROXY header is not present, subsequent reads do not. It is the task
+ // of the code using the connection to handle that case properly.
+ REQUIRE
+ // SKIP accepts a connection without requiring the PROXY header.
+ // Note: an example usage can be found in the SkipProxyHeaderForCIDR
+ // function.
+ SKIP
+)
+
+// ConnSkipProxyHeaderForCIDR returns a ConnPolicyFunc which can be used to accept
+// a connection from a skipHeaderCIDR without requiring a PROXY header, e.g.
+// Kubernetes pods local traffic. The def is a policy to use when an upstream
+// address doesn't match the skipHeaderCIDR.
+func ConnSkipProxyHeaderForCIDR(skipHeaderCIDR *net.IPNet, def Policy) ConnPolicyFunc {
+ return func(connOpts ConnPolicyOptions) (Policy, error) {
+ ip, err := ipFromAddr(connOpts.Upstream)
+ if err != nil {
+ return def, err
+ }
+
+ if skipHeaderCIDR != nil && skipHeaderCIDR.Contains(ip) {
+ return SKIP, nil
+ }
+
+ return def, nil
+ }
+}
+
+// SkipProxyHeaderForCIDR returns a PolicyFunc which can be used to accept a
+// connection from a skipHeaderCIDR without requiring a PROXY header, e.g.
+// Kubernetes pods local traffic. The def is a policy to use when an upstream
+// address doesn't match the skipHeaderCIDR.
+//
+// Deprecated: use ConnSkipProxyHeaderForCIDR instead.
+func SkipProxyHeaderForCIDR(skipHeaderCIDR *net.IPNet, def Policy) PolicyFunc {
+ connPolicy := ConnSkipProxyHeaderForCIDR(skipHeaderCIDR, def)
+ return func(upstream net.Addr) (Policy, error) {
+ return connPolicy(ConnPolicyOptions{Upstream: upstream})
+ }
+}
+
+// WithPolicy adds given policy to a connection when passed as option to NewConn().
+func WithPolicy(p Policy) func(*Conn) {
+ return func(c *Conn) {
+ c.ProxyHeaderPolicy = p
+ }
+}
+
+// ConnLaxWhiteListPolicy returns a ConnPolicyFunc which decides whether the
+// upstream ip is allowed to send a proxy header based on a list of allowed
+// IP addresses and IP ranges. In case upstream IP is not in list the proxy
+// header will be ignored. If one of the provided IP addresses or IP ranges
+// is invalid it will return an error instead of a ConnPolicyFunc.
+func ConnLaxWhiteListPolicy(allowed []string) (ConnPolicyFunc, error) {
+ allowFrom, err := parse(allowed)
+ if err != nil {
+ return nil, err
+ }
+
+ return connWhitelistPolicy(allowFrom, IGNORE), nil
+}
+
+// LaxWhiteListPolicy returns a PolicyFunc which decides whether the
+// upstream ip is allowed to send a proxy header based on a list of allowed
+// IP addresses and IP ranges. In case upstream IP is not in list the proxy
+// header will be ignored. If one of the provided IP addresses or IP ranges
+// is invalid it will return an error instead of a PolicyFunc.
+//
+// Deprecated: use ConnLaxWhiteListPolicy instead.
+func LaxWhiteListPolicy(allowed []string) (PolicyFunc, error) {
+ connPolicy, err := ConnLaxWhiteListPolicy(allowed)
+ if err != nil {
+ return nil, err
+ }
+
+ return func(upstream net.Addr) (Policy, error) {
+ return connPolicy(ConnPolicyOptions{Upstream: upstream})
+ }, nil
+}
+
+// ConnMustLaxWhiteListPolicy returns a ConnLaxWhiteListPolicy but will panic
+// if one of the provided IP addresses or IP ranges is invalid.
+func ConnMustLaxWhiteListPolicy(allowed []string) ConnPolicyFunc {
+ pfunc, err := ConnLaxWhiteListPolicy(allowed)
+ if err != nil {
+ panic(err)
+ }
+
+ return pfunc
+}
+
+// MustLaxWhiteListPolicy returns a LaxWhiteListPolicy but will panic if one
+// of the provided IP addresses or IP ranges is invalid.
+//
+// Deprecated: use ConnMustLaxWhiteListPolicy instead.
+func MustLaxWhiteListPolicy(allowed []string) PolicyFunc {
+ connPolicy := ConnMustLaxWhiteListPolicy(allowed)
+ return func(upstream net.Addr) (Policy, error) {
+ return connPolicy(ConnPolicyOptions{Upstream: upstream})
+ }
+}
+
+// ConnStrictWhiteListPolicy returns a ConnPolicyFunc which decides whether the
+// upstream ip is allowed to send a proxy header based on a list of allowed
+// IP addresses and IP ranges. In case upstream IP is not in list reading on
+// the connection will be refused on the first read. Please note: subsequent
+// reads do not error. It is the task of the code using the connection to
+// handle that case properly. If one of the provided IP addresses or IP
+// ranges is invalid it will return an error instead of a ConnPolicyFunc.
+func ConnStrictWhiteListPolicy(allowed []string) (ConnPolicyFunc, error) {
+ allowFrom, err := parse(allowed)
+ if err != nil {
+ return nil, err
+ }
+
+ return connWhitelistPolicy(allowFrom, REJECT), nil
+}
+
+// StrictWhiteListPolicy returns a PolicyFunc which decides whether the
+// upstream ip is allowed to send a proxy header based on a list of allowed
+// IP addresses and IP ranges. In case upstream IP is not in list reading on
+// the connection will be refused on the first read. Please note: subsequent
+// reads do not error. It is the task of the code using the connection to
+// handle that case properly. If one of the provided IP addresses or IP
+// ranges is invalid it will return an error instead of a PolicyFunc.
+//
+// Deprecated: use ConnStrictWhiteListPolicy instead.
+func StrictWhiteListPolicy(allowed []string) (PolicyFunc, error) {
+ connPolicy, err := ConnStrictWhiteListPolicy(allowed)
+ if err != nil {
+ return nil, err
+ }
+
+ return func(upstream net.Addr) (Policy, error) {
+ return connPolicy(ConnPolicyOptions{Upstream: upstream})
+ }, nil
+}
+
+// ConnMustStrictWhiteListPolicy returns a ConnStrictWhiteListPolicy but will panic
+// if one of the provided IP addresses or IP ranges is invalid.
+func ConnMustStrictWhiteListPolicy(allowed []string) ConnPolicyFunc {
+ pfunc, err := ConnStrictWhiteListPolicy(allowed)
+ if err != nil {
+ panic(err)
+ }
+
+ return pfunc
+}
+
+// MustStrictWhiteListPolicy returns a StrictWhiteListPolicy but will panic
+// if one of the provided IP addresses or IP ranges is invalid.
+//
+// Deprecated: use ConnMustStrictWhiteListPolicy instead.
+func MustStrictWhiteListPolicy(allowed []string) PolicyFunc {
+ connPolicy := ConnMustStrictWhiteListPolicy(allowed)
+ return func(upstream net.Addr) (Policy, error) {
+ return connPolicy(ConnPolicyOptions{Upstream: upstream})
+ }
+}
+
+func connWhitelistPolicy(allowed []func(net.IP) bool, def Policy) ConnPolicyFunc {
+ return func(connOpts ConnPolicyOptions) (Policy, error) {
+ upstreamIP, err := ipFromAddr(connOpts.Upstream)
+ if err != nil {
+ // something is wrong with the source IP, better reject the connection
+ return REJECT, err
+ }
+
+ for _, allowFrom := range allowed {
+ if allowFrom(upstreamIP) {
+ return USE, nil
+ }
+ }
+
+ return def, nil
+ }
+}
+
+func parse(allowed []string) ([]func(net.IP) bool, error) {
+ a := make([]func(net.IP) bool, len(allowed))
+ for i, allowFrom := range allowed {
+ if strings.LastIndex(allowFrom, "/") > 0 {
+ _, ipRange, err := net.ParseCIDR(allowFrom)
+ if err != nil {
+ return nil, fmt.Errorf("proxyproto: given string %q is not a valid IP range: %v", allowFrom, err)
+ }
+
+ a[i] = ipRange.Contains
+ } else {
+ allowed := net.ParseIP(allowFrom)
+ if allowed == nil {
+ return nil, fmt.Errorf("proxyproto: given string %q is not a valid IP address", allowFrom)
+ }
+
+ a[i] = allowed.Equal
+ }
+ }
+
+ return a, nil
+}
+
+func ipFromAddr(upstream net.Addr) (net.IP, error) {
+ upstreamString, _, err := net.SplitHostPort(upstream.String())
+ if err != nil {
+ return nil, err
+ }
+
+ upstreamIP := net.ParseIP(upstreamString)
+ if nil == upstreamIP {
+ return nil, fmt.Errorf("proxyproto: invalid IP address")
+ }
+
+ return upstreamIP, nil
+}
+
+// TrustProxyHeaderFrom returns a ConnPolicyFunc which can be used to decide
+// whether to use or reject PROXY headers based on the source IP of the
+// connection. This policy ensures that only trusted sources can set the PROXY
+// header. Connections from IPs not in the trusted list will be rejected.
+func TrustProxyHeaderFrom(trustedIPs ...net.IP) ConnPolicyFunc {
+ return func(connOpts ConnPolicyOptions) (Policy, error) {
+ ip, err := ipFromAddr(connOpts.Upstream)
+ if err != nil {
+ return REJECT, err
+ }
+
+ for _, trustedIP := range trustedIPs {
+ if trustedIP.Equal(ip) {
+ return USE, nil
+ }
+ }
+
+ return REJECT, nil
+ }
+}
+
+// IgnoreProxyHeaderNotOnInterface returns a ConnPolicyFunc which can be used to
+// decide whether to use or ignore PROXY headers depending on the connection
+// being made on specific interfaces. This policy can be used when the server
+// is bound to multiple interfaces but wants to allow on one or more interfaces.
+func IgnoreProxyHeaderNotOnInterface(allowedIP net.IP) ConnPolicyFunc {
+ return func(connOpts ConnPolicyOptions) (Policy, error) {
+ ip, err := ipFromAddr(connOpts.Downstream)
+ if err != nil {
+ return REJECT, err
+ }
+
+ if allowedIP.Equal(ip) {
+ return USE, nil
+ }
+
+ return IGNORE, nil
+ }
+}
diff --git a/vendor/github.com/pires/go-proxyproto/protocol.go b/vendor/github.com/pires/go-proxyproto/protocol.go
new file mode 100644
index 0000000000..41bc30d454
--- /dev/null
+++ b/vendor/github.com/pires/go-proxyproto/protocol.go
@@ -0,0 +1,491 @@
+package proxyproto
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "sync"
+ "sync/atomic"
+ "time"
+)
+
+// readBufferSize is the size used for bufio.Reader's internal buffer.
+//
+// This is kept low to reduce per-connection memory overhead. If the header is
+// larger than readBufferSize, the header will be decoded with multiple Read
+// calls. For v1 the header length is at most 108 bytes. For v2 the header
+// length is at most 52 bytes plus the length of the TLVs. We use 256 bytes to
+// accommodate for the most common cases.
+const readBufferSize = 256
+
+var (
+ // DefaultReadHeaderTimeout is how long header processing waits for header to
+ // be read from the wire, if Listener.ReaderHeaderTimeout is not set.
+ // It's kept as a global variable so to make it easier to find and override,
+ // e.g. go build -ldflags -X "github.com/pires/go-proxyproto.DefaultReadHeaderTimeout=1s".
+ DefaultReadHeaderTimeout = 10 * time.Second
+
+ // ErrInvalidUpstream should be returned when an upstream connection address
+ // is not trusted, and therefore is invalid.
+ ErrInvalidUpstream = fmt.Errorf("proxyproto: upstream connection address not trusted for PROXY information")
+)
+
+// Listener is used to wrap an underlying listener,
+// whose connections may be using the HAProxy Proxy Protocol.
+// If the connection is using the protocol, the RemoteAddr() will return
+// the correct client address. ReadHeaderTimeout will be applied to all
+// connections in order to prevent blocking operations. If no ReadHeaderTimeout
+// is set, a default of 10s will be used. This can be disabled by setting the
+// timeout to < 0.
+//
+// Only one of Policy or ConnPolicy should be provided. If both are provided then
+// a panic would occur during accept.
+type Listener struct {
+ // Listener is the underlying listener.
+ Listener net.Listener
+ // Deprecated: use ConnPolicyFunc instead. This will be removed in future release.
+ Policy PolicyFunc
+ // ConnPolicy is the policy function for accepted connections.
+ ConnPolicy ConnPolicyFunc
+ // ValidateHeader is the validator function for the proxy header.
+ ValidateHeader Validator
+ // ReadHeaderTimeout is the timeout for reading the proxy header.
+ ReadHeaderTimeout time.Duration
+ // ReadBufferSize is the read buffer size for accepted connections. When > 0,
+ // each accepted connection uses this size for proxy header detection; 0 means default.
+ ReadBufferSize int
+}
+
+// Conn is used to wrap and underlying connection which
+// may be speaking the Proxy Protocol. If it is, the RemoteAddr() will
+// return the address of the client instead of the proxy address. Each connection
+// will have its own readHeaderTimeout and readDeadline set by the Accept() call.
+type Conn struct {
+ readDeadline atomic.Value // time.Time
+ once sync.Once
+ readErr error
+ conn net.Conn
+ bufReader *bufio.Reader
+ // bufferSize is set when the client overrides via WithBufferSize; nil means use default.
+ bufferSize *int
+ header *Header
+ ProxyHeaderPolicy Policy
+ Validate Validator
+ readHeaderTimeout time.Duration
+}
+
+// Validator receives a header and decides whether it is a valid one
+// In case the header is not deemed valid it should return an error.
+type Validator func(*Header) error
+
+// ValidateHeader adds given validator for proxy headers to a connection when passed as option to NewConn().
+func ValidateHeader(v Validator) func(*Conn) {
+ return func(c *Conn) {
+ if v != nil {
+ c.Validate = v
+ }
+ }
+}
+
+// SetReadHeaderTimeout sets the readHeaderTimeout for a connection when passed as option to NewConn().
+func SetReadHeaderTimeout(t time.Duration) func(*Conn) {
+ return func(c *Conn) {
+ if t >= 0 {
+ c.readHeaderTimeout = t
+ }
+ }
+}
+
+// WithBufferSize sets the size of the read buffer used for proxy header detection.
+// Values <= 0 are ignored and the default (256 bytes) is used. Values < 16 are
+// effectively 16 due to bufio's minimum. The default is tuned for typical proxy
+// protocol header lengths.
+func WithBufferSize(length int) func(*Conn) {
+ return func(c *Conn) {
+ if length <= 0 {
+ return
+ }
+ p := new(int)
+ *p = length
+ c.bufferSize = p
+ c.bufReader = bufio.NewReaderSize(c.conn, length)
+ }
+}
+
+// Accept waits for and returns the next valid connection to the listener.
+func (p *Listener) Accept() (net.Conn, error) {
+ for {
+ // Get the underlying connection.
+ conn, err := p.Listener.Accept()
+ if err != nil {
+ return nil, err
+ }
+
+ proxyHeaderPolicy := USE
+ if p.Policy != nil && p.ConnPolicy != nil {
+ panic("only one of policy or connpolicy must be provided.")
+ }
+ if p.Policy != nil || p.ConnPolicy != nil {
+ if p.Policy != nil {
+ proxyHeaderPolicy, err = p.Policy(conn.RemoteAddr())
+ } else {
+ proxyHeaderPolicy, err = p.ConnPolicy(ConnPolicyOptions{
+ Upstream: conn.RemoteAddr(),
+ Downstream: conn.LocalAddr(),
+ })
+ }
+ if err != nil {
+ // can't decide the policy, we can't accept the connection.
+ if closeErr := conn.Close(); closeErr != nil {
+ return nil, closeErr
+ }
+
+ if errors.Is(err, ErrInvalidUpstream) {
+ // keep listening for other connections.
+ continue
+ }
+
+ return nil, err
+ }
+ // Handle a connection as a regular one.
+ if proxyHeaderPolicy == SKIP {
+ return conn, nil
+ }
+ }
+
+ opts := []func(*Conn){
+ WithPolicy(proxyHeaderPolicy),
+ ValidateHeader(p.ValidateHeader),
+ }
+ if p.ReadBufferSize > 0 {
+ opts = append(opts, WithBufferSize(p.ReadBufferSize))
+ }
+ newConn := NewConn(conn, opts...)
+
+ // If the ReadHeaderTimeout for the listener is unset, use the default timeout.
+ if p.ReadHeaderTimeout == 0 {
+ p.ReadHeaderTimeout = DefaultReadHeaderTimeout
+ }
+
+ // Set the readHeaderTimeout of the new conn to the value of the listener
+ newConn.readHeaderTimeout = p.ReadHeaderTimeout
+
+ return newConn, nil
+ }
+}
+
+// Close closes the underlying listener.
+func (p *Listener) Close() error {
+ return p.Listener.Close()
+}
+
+// Addr returns the underlying listener's network address.
+func (p *Listener) Addr() net.Addr {
+ return p.Listener.Addr()
+}
+
+// NewConn is used to wrap a net.Conn that may be speaking the PROXY protocol
+// into a proxyproto.Conn.
+//
+// NOTE: NewConn may interfere with previously set ReadDeadline on the provided net.Conn,
+// because it sets a temporary deadline when detecting and reading the PROXY protocol header.
+// If you need to enforce a specific ReadDeadline on the connection, be sure to call Conn.SetReadDeadline
+// again after NewConn returns, to restore your desired deadline.
+func NewConn(conn net.Conn, opts ...func(*Conn)) *Conn {
+ br := bufio.NewReaderSize(conn, readBufferSize)
+
+ pConn := &Conn{
+ bufReader: br,
+ conn: conn,
+ }
+
+ for _, opt := range opts {
+ opt(pConn)
+ }
+
+ return pConn
+}
+
+// Read is check for the proxy protocol header when doing
+// the initial scan. If there is an error parsing the header,
+// it is returned and the socket is closed.
+func (p *Conn) Read(b []byte) (int, error) {
+ // Ensure header processing runs at most once and surface any errors.
+ if err := p.ensureHeaderProcessed(); err != nil {
+ return 0, err
+ }
+
+ // Drain the buffer if it exists and has data.
+ if p.bufReader != nil {
+ if p.bufReader.Buffered() > 0 {
+ n, err := p.bufReader.Read(b)
+
+ // Did we empty the buffer?
+ // Buffering a net.Conn means the buffer doesn't return io.EOF until the connection returns io.EOF.
+ // Therefore, we use Buffered() == 0 to detect if we are done with the buffer.
+ if p.bufReader.Buffered() == 0 {
+ // Garbage collect the buffer.
+ p.bufReader = nil
+ }
+
+ // Return immediately. Do not touch p.conn.
+ // If err is EOF here, it means the connection is actually closed,
+ // so we should return that error to the user anyway.
+ return n, err
+ }
+ // If buffer was empty to begin with (shouldn't happen with the >0 check
+ // but good for safety), clear it.
+ p.bufReader = nil
+ }
+
+ // From now on, read directly from the underlying connection.
+ return p.conn.Read(b)
+}
+
+// Write wraps original conn.Write.
+func (p *Conn) Write(b []byte) (int, error) {
+ // Ensure header processing has completed before writing.
+ if err := p.ensureHeaderProcessed(); err != nil {
+ return 0, err
+ }
+ return p.conn.Write(b)
+}
+
+// Close wraps original conn.Close.
+func (p *Conn) Close() error {
+ return p.conn.Close()
+}
+
+// ProxyHeader returns the proxy protocol header, if any. If an error occurs
+// while reading the proxy header, nil is returned.
+func (p *Conn) ProxyHeader() *Header {
+ // Ensure header processing runs at most once.
+ _ = p.ensureHeaderProcessed()
+ return p.header
+}
+
+// LocalAddr returns the address of the server if the proxy
+// protocol is being used, otherwise just returns the address of
+// the socket server. In case an error happens on reading the
+// proxy header the original LocalAddr is returned, not the one
+// from the proxy header even if the proxy header itself is
+// syntactically correct.
+func (p *Conn) LocalAddr() net.Addr {
+ // Ensure header processing runs at most once.
+ _ = p.ensureHeaderProcessed()
+ if p.header == nil || p.header.Command.IsLocal() || p.readErr != nil {
+ return p.conn.LocalAddr()
+ }
+
+ return p.header.DestinationAddr
+}
+
+// RemoteAddr returns the address of the client if the proxy
+// protocol is being used, otherwise just returns the address of
+// the socket peer. In case an error happens on reading the
+// proxy header the original RemoteAddr is returned, not the one
+// from the proxy header even if the proxy header itself is
+// syntactically correct.
+func (p *Conn) RemoteAddr() net.Addr {
+ // Ensure header processing runs at most once.
+ _ = p.ensureHeaderProcessed()
+ if p.header == nil || p.header.Command.IsLocal() || p.readErr != nil {
+ return p.conn.RemoteAddr()
+ }
+
+ return p.header.SourceAddr
+}
+
+// Raw returns the underlying connection which can be casted to
+// a concrete type, allowing access to specialized functions.
+//
+// Use this ONLY if you know exactly what you are doing.
+func (p *Conn) Raw() net.Conn {
+ return p.conn
+}
+
+// TCPConn returns the underlying TCP connection,
+// allowing access to specialized functions.
+//
+// Use this ONLY if you know exactly what you are doing.
+func (p *Conn) TCPConn() (conn *net.TCPConn, ok bool) {
+ conn, ok = p.conn.(*net.TCPConn)
+ return
+}
+
+// UnixConn returns the underlying Unix socket connection,
+// allowing access to specialized functions.
+//
+// Use this ONLY if you know exactly what you are doing.
+func (p *Conn) UnixConn() (conn *net.UnixConn, ok bool) {
+ conn, ok = p.conn.(*net.UnixConn)
+ return
+}
+
+// UDPConn returns the underlying UDP connection,
+// allowing access to specialized functions.
+//
+// Use this ONLY if you know exactly what you are doing.
+func (p *Conn) UDPConn() (conn *net.UDPConn, ok bool) {
+ conn, ok = p.conn.(*net.UDPConn)
+ return
+}
+
+// SetDeadline wraps original conn.SetDeadline.
+func (p *Conn) SetDeadline(t time.Time) error {
+ p.readDeadline.Store(t)
+ return p.conn.SetDeadline(t)
+}
+
+// SetReadDeadline wraps original conn.SetReadDeadline.
+func (p *Conn) SetReadDeadline(t time.Time) error {
+ // Set a local var that tells us the desired deadline. This is
+ // needed in order to reset the read deadline to the one that is
+ // desired by the user, rather than an empty deadline.
+ p.readDeadline.Store(t)
+ return p.conn.SetReadDeadline(t)
+}
+
+// SetWriteDeadline wraps original conn.SetWriteDeadline.
+func (p *Conn) SetWriteDeadline(t time.Time) error {
+ return p.conn.SetWriteDeadline(t)
+}
+
+// readHeader reads the proxy protocol header from the connection.
+func (p *Conn) readHeader() error {
+ // If the connection's readHeaderTimeout is more than 0,
+ // apply a temporary deadline without extending a user-configured
+ // deadline. If the user has no deadline, we use now + timeout.
+ if p.readHeaderTimeout > 0 {
+ var (
+ storedDeadline time.Time
+ hasDeadline bool
+ )
+ if t := p.readDeadline.Load(); t != nil {
+ storedDeadline = t.(time.Time)
+ hasDeadline = !storedDeadline.IsZero()
+ }
+
+ headerDeadline := time.Now().Add(p.readHeaderTimeout)
+ if hasDeadline && storedDeadline.Before(headerDeadline) {
+ // Clamp to the user's earlier deadline to avoid extending it.
+ headerDeadline = storedDeadline
+ }
+
+ if err := p.conn.SetReadDeadline(headerDeadline); err != nil {
+ return err
+ }
+ }
+
+ header, err := Read(p.bufReader)
+
+ // If the connection's readHeaderTimeout is more than 0, undo the change to the
+ // deadline that we made above. Because we retain the readDeadline as part of our
+ // SetReadDeadline override, we can restore the user's deadline (if any).
+ // Therefore, we check whether the error is a net.Timeout and if it is, we decide
+ // the proxy proto does not exist and set the error accordingly.
+ if p.readHeaderTimeout > 0 {
+ t := p.readDeadline.Load()
+ if t == nil {
+ t = time.Time{}
+ }
+ if err := p.conn.SetReadDeadline(t.(time.Time)); err != nil {
+ return err
+ }
+ if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
+ err = ErrNoProxyProtocol
+ }
+ }
+
+ // For the purpose of this wrapper shamefully stolen from armon/go-proxyproto
+ // let's act as if there was no error when PROXY protocol is not present.
+ if err == ErrNoProxyProtocol {
+ // but not if it is required that the connection has one
+ if p.ProxyHeaderPolicy == REQUIRE {
+ return err
+ }
+
+ return nil
+ }
+
+ // proxy protocol header was found
+ if err == nil && header != nil {
+ switch p.ProxyHeaderPolicy {
+ case REJECT:
+ // this connection is not allowed to send one
+ return ErrSuperfluousProxyHeader
+ case USE, REQUIRE:
+ if p.Validate != nil {
+ err = p.Validate(header)
+ if err != nil {
+ return err
+ }
+ }
+
+ p.header = header
+ }
+ }
+
+ return err
+}
+
+// ensureHeaderProcessed runs header processing once.
+func (p *Conn) ensureHeaderProcessed() error {
+ p.once.Do(func() {
+ p.readErr = p.readHeader()
+ })
+ if p.readErr != nil {
+ return p.readErr
+ }
+ return nil
+}
+
+// ReadFrom implements the io.ReaderFrom ReadFrom method.
+func (p *Conn) ReadFrom(r io.Reader) (int64, error) {
+ // Ensure header processing has completed before reading/writing.
+ if err := p.ensureHeaderProcessed(); err != nil {
+ return 0, err
+ }
+ if rf, ok := p.conn.(io.ReaderFrom); ok {
+ return rf.ReadFrom(r)
+ }
+ return io.Copy(p.conn, r)
+}
+
+// WriteTo implements io.WriterTo.
+func (p *Conn) WriteTo(w io.Writer) (int64, error) {
+ // Ensure header processing has completed before reading/writing.
+ if err := p.ensureHeaderProcessed(); err != nil {
+ return 0, err
+ }
+
+ // If the buffer has been drained (or cleared), copy directly from conn.
+ if p.bufReader == nil {
+ return io.Copy(w, p.conn)
+ }
+
+ b := make([]byte, p.bufReader.Buffered())
+ if _, err := p.bufReader.Read(b); err != nil {
+ return 0, err // this should never happen as we read buffered data.
+ }
+
+ var n int64
+ {
+ nn, err := w.Write(b)
+ n += int64(nn)
+ if err != nil {
+ return n, err
+ }
+ }
+ {
+ nn, err := io.Copy(w, p.conn)
+ n += nn
+ if err != nil {
+ return n, err
+ }
+ }
+
+ return n, nil
+}
diff --git a/vendor/github.com/pires/go-proxyproto/tlv.go b/vendor/github.com/pires/go-proxyproto/tlv.go
new file mode 100644
index 0000000000..290df89092
--- /dev/null
+++ b/vendor/github.com/pires/go-proxyproto/tlv.go
@@ -0,0 +1,144 @@
+// Type-Length-Value splitting and parsing for proxy protocol V2
+// See spec https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt sections 2.2 to 2.7 and
+
+package proxyproto
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "math"
+)
+
+// TLV type constants defined by the PROXY protocol spec.
+//
+//nolint:revive // Names follow the spec.
+const (
+ // Section 2.2.
+ PP2_TYPE_ALPN PP2Type = 0x01
+ PP2_TYPE_AUTHORITY PP2Type = 0x02
+ PP2_TYPE_CRC32C PP2Type = 0x03
+ PP2_TYPE_NOOP PP2Type = 0x04
+ PP2_TYPE_UNIQUE_ID PP2Type = 0x05
+ PP2_TYPE_SSL PP2Type = 0x20
+ PP2_SUBTYPE_SSL_VERSION PP2Type = 0x21
+ PP2_SUBTYPE_SSL_CN PP2Type = 0x22
+ PP2_SUBTYPE_SSL_CIPHER PP2Type = 0x23
+ PP2_SUBTYPE_SSL_SIG_ALG PP2Type = 0x24
+ PP2_SUBTYPE_SSL_KEY_ALG PP2Type = 0x25
+ PP2_SUBTYPE_SSL_GROUP PP2Type = 0x26
+ PP2_SUBTYPE_SSL_SIG_SCHEME PP2Type = 0x27
+ PP2_SUBTYPE_SSL_CLIENT_CERT PP2Type = 0x28
+ PP2_TYPE_NETNS PP2Type = 0x30
+
+ // Section 2.2.7, reserved types.
+ PP2_TYPE_MIN_CUSTOM PP2Type = 0xE0
+ PP2_TYPE_MAX_CUSTOM PP2Type = 0xEF
+ PP2_TYPE_MIN_EXPERIMENT PP2Type = 0xF0
+ PP2_TYPE_MAX_EXPERIMENT PP2Type = 0xF7
+ PP2_TYPE_MIN_FUTURE PP2Type = 0xF8
+ PP2_TYPE_MAX_FUTURE PP2Type = 0xFF
+)
+
+var (
+ // ErrTruncatedTLV indicates a TLV was truncated.
+ ErrTruncatedTLV = errors.New("proxyproto: truncated TLV")
+ // ErrMalformedTLV indicates a TLV has malformed data.
+ ErrMalformedTLV = errors.New("proxyproto: malformed TLV Value")
+ // ErrIncompatibleTLV indicates a TLV is of an unexpected type.
+ ErrIncompatibleTLV = errors.New("proxyproto: incompatible TLV type")
+)
+
+// PP2Type is the proxy protocol v2 type.
+type PP2Type byte
+
+// TLV is a uninterpreted Type-Length-Value for V2 protocol, see section 2.2.
+type TLV struct {
+ Type PP2Type
+ Value []byte
+}
+
+// SplitTLVs splits the Type-Length-Value vector, returns the vector or an error.
+func SplitTLVs(raw []byte) ([]TLV, error) {
+ var tlvs []TLV
+ for i := 0; i < len(raw); {
+ tlv := TLV{
+ Type: PP2Type(raw[i]),
+ }
+ if len(raw)-i <= 2 {
+ return nil, ErrTruncatedTLV
+ }
+ tlvLen := int(binary.BigEndian.Uint16(raw[i+1 : i+3])) // Max length = 65K
+ i += 3
+ if i+tlvLen > len(raw) {
+ return nil, ErrTruncatedTLV
+ }
+ // Ignore no-op padding
+ if tlv.Type != PP2_TYPE_NOOP {
+ tlv.Value = make([]byte, tlvLen)
+ copy(tlv.Value, raw[i:i+tlvLen])
+ }
+ i += tlvLen
+ tlvs = append(tlvs, tlv)
+ }
+ return tlvs, nil
+}
+
+// JoinTLVs joins multiple Type-Length-Value records.
+func JoinTLVs(tlvs []TLV) ([]byte, error) {
+ var raw []byte
+ for _, tlv := range tlvs {
+ if len(tlv.Value) > math.MaxUint16 {
+ return nil, fmt.Errorf("proxyproto: cannot format TLV %v with length %d", tlv.Type, len(tlv.Value))
+ }
+ var length [2]byte
+ //nolint:gosec // lengthValue is validated above.
+ lengthValue := uint16(len(tlv.Value))
+ binary.BigEndian.PutUint16(length[:], lengthValue)
+ raw = append(raw, byte(tlv.Type))
+ raw = append(raw, length[:]...)
+ raw = append(raw, tlv.Value...)
+ }
+ return raw, nil
+}
+
+// Registered is true if the type is registered in the spec, see section 2.2.
+func (p PP2Type) Registered() bool {
+ switch p {
+ case PP2_TYPE_ALPN,
+ PP2_TYPE_AUTHORITY,
+ PP2_TYPE_CRC32C,
+ PP2_TYPE_NOOP,
+ PP2_TYPE_UNIQUE_ID,
+ PP2_TYPE_SSL,
+ PP2_SUBTYPE_SSL_VERSION,
+ PP2_SUBTYPE_SSL_CN,
+ PP2_SUBTYPE_SSL_CIPHER,
+ PP2_SUBTYPE_SSL_SIG_ALG,
+ PP2_SUBTYPE_SSL_KEY_ALG,
+ PP2_TYPE_NETNS:
+ return true
+ }
+ return false
+}
+
+// App is true if the type is reserved for application specific data, see section 2.2.7.
+func (p PP2Type) App() bool {
+ return p >= PP2_TYPE_MIN_CUSTOM && p <= PP2_TYPE_MAX_CUSTOM
+}
+
+// Experiment is true if the type is reserved for temporary experimental use by application
+// developers, see section 2.2.7.
+func (p PP2Type) Experiment() bool {
+ return p >= PP2_TYPE_MIN_EXPERIMENT && p <= PP2_TYPE_MAX_EXPERIMENT
+}
+
+// Future is true is the type is reserved for future use, see section 2.2.7.
+func (p PP2Type) Future() bool {
+ return p >= PP2_TYPE_MIN_FUTURE
+}
+
+// Spec is true if the type is covered by the spec, see section 2.2 and 2.2.7.
+func (p PP2Type) Spec() bool {
+ return p.Registered() || p.App() || p.Experiment() || p.Future()
+}
diff --git a/vendor/github.com/pires/go-proxyproto/v1.go b/vendor/github.com/pires/go-proxyproto/v1.go
new file mode 100644
index 0000000000..8574d4a7c8
--- /dev/null
+++ b/vendor/github.com/pires/go-proxyproto/v1.go
@@ -0,0 +1,258 @@
+package proxyproto
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "net"
+ "net/netip"
+ "strconv"
+ "strings"
+)
+
+const (
+ crlf = "\r\n"
+ separator = " "
+)
+
+func initVersion1() *Header {
+ header := new(Header)
+ header.Version = 1
+ // Command doesn't exist in v1
+ header.Command = PROXY
+ return header
+}
+
+func parseVersion1(reader *bufio.Reader) (*Header, error) {
+ //The header cannot be more than 107 bytes long. Per spec:
+ //
+ // (...)
+ // - worst case (optional fields set to 0xff) :
+ // "PROXY UNKNOWN ffff:f...f:ffff ffff:f...f:ffff 65535 65535\r\n"
+ // => 5 + 1 + 7 + 1 + 39 + 1 + 39 + 1 + 5 + 1 + 5 + 2 = 107 chars
+ //
+ // So a 108-byte buffer is always enough to store all the line and a
+ // trailing zero for string processing.
+ //
+ // It must also be CRLF terminated, as above. The header does not otherwise
+ // contain a CR or LF byte.
+ //
+ // ISSUE #69
+ // We can't use Peek here as it will block trying to fill the buffer, which
+ // will never happen if the header is TCP4 or TCP6 (max. 56 and 104 bytes
+ // respectively) and the server is expected to speak first.
+ //
+ // Similarly, we can't use ReadString or ReadBytes as these will keep reading
+ // until the delimiter is found; an abusive client could easily disrupt a
+ // server by sending a large amount of data that do not contain a LF byte.
+ // Another means of attack would be to start connections and simply not send
+ // data after the initial PROXY signature bytes, accumulating a large
+ // number of blocked goroutines on the server. ReadSlice will also block for
+ // a delimiter when the internal buffer does not fill up.
+ //
+ // A plain Read is also problematic since we risk reading past the end of the
+ // header without being able to easily put the excess bytes back into the reader's
+ // buffer (with the current implementation's design).
+ //
+ // So we use a ReadByte loop, which solves the overflow problem and avoids
+ // reading beyond the end of the header. However, we need one more trick to harden
+ // against partial header attacks (slow loris) - per spec:
+ //
+ // (..) The sender must always ensure that the header is sent at once, so that
+ // the transport layer maintains atomicity along the path to the receiver. The
+ // receiver may be tolerant to partial headers or may simply drop the connection
+ // when receiving a partial header. Recommendation is to be tolerant, but
+ // implementation constraints may not always easily permit this.
+ //
+ // We are subject to such implementation constraints. So we return an error if
+ // the header cannot be fully extracted with a single read of the underlying
+ // reader.
+ buf := make([]byte, 0, 107)
+ for {
+ b, err := reader.ReadByte()
+ if err != nil {
+ return nil, fmt.Errorf("%w: %w", ErrCantReadVersion1Header, err)
+ }
+ buf = append(buf, b)
+ if b == '\n' {
+ // End of header found
+ break
+ }
+ if len(buf) == 107 {
+ // No delimiter in first 107 bytes
+ return nil, ErrVersion1HeaderTooLong
+ }
+ if reader.Buffered() == 0 {
+ // Header was not buffered in a single read. Since we can't
+ // differentiate between genuine slow writers and DoS agents,
+ // we abort. On healthy networks, this should never happen.
+ return nil, ErrCantReadVersion1Header
+ }
+ }
+
+ // Check for CR before LF.
+ if len(buf) < 2 || buf[len(buf)-2] != '\r' {
+ return nil, ErrLineMustEndWithCrlf
+ }
+
+ // Check full signature.
+ tokens := strings.Split(string(buf[:len(buf)-2]), separator)
+
+ // Expect at least 2 tokens: "PROXY" and the transport protocol.
+ if len(tokens) < 2 {
+ return nil, ErrCantReadAddressFamilyAndProtocol
+ }
+
+ // Read address family and protocol
+ var transportProtocol AddressFamilyAndProtocol
+ switch tokens[1] {
+ case "TCP4":
+ transportProtocol = TCPv4
+ case "TCP6":
+ transportProtocol = TCPv6
+ case "UNKNOWN":
+ transportProtocol = UNSPEC // doesn't exist in v1 but fits UNKNOWN
+ default:
+ return nil, ErrCantReadAddressFamilyAndProtocol
+ }
+
+ // Expect 6 tokens only when UNKNOWN is not present.
+ if transportProtocol != UNSPEC && len(tokens) < 6 {
+ return nil, ErrCantReadAddressFamilyAndProtocol
+ }
+
+ // When a signature is found, allocate a v1 header with Command set to PROXY.
+ // Command doesn't exist in v1 but set it for other parts of this library
+ // to rely on it for determining connection details.
+ header := initVersion1()
+
+ // Transport protocol has been processed already.
+ header.TransportProtocol = transportProtocol
+
+ // When UNKNOWN, set the command to LOCAL and return early
+ if header.TransportProtocol == UNSPEC {
+ header.Command = LOCAL
+ return header, nil
+ }
+
+ // Otherwise, continue to read addresses and ports
+ sourceIP, err := parseV1IPAddress(header.TransportProtocol, tokens[2])
+ if err != nil {
+ return nil, err
+ }
+ destIP, err := parseV1IPAddress(header.TransportProtocol, tokens[3])
+ if err != nil {
+ return nil, err
+ }
+ sourcePort, err := parseV1PortNumber(tokens[4])
+ if err != nil {
+ return nil, err
+ }
+ destPort, err := parseV1PortNumber(tokens[5])
+ if err != nil {
+ return nil, err
+ }
+ header.SourceAddr = &net.TCPAddr{
+ IP: sourceIP,
+ Port: sourcePort,
+ }
+ header.DestinationAddr = &net.TCPAddr{
+ IP: destIP,
+ Port: destPort,
+ }
+
+ return header, nil
+}
+
+func (header *Header) formatVersion1() ([]byte, error) {
+ // As of version 1, only "TCP4" ( \x54 \x43 \x50 \x34 ) for TCP over IPv4,
+ // and "TCP6" ( \x54 \x43 \x50 \x36 ) for TCP over IPv6 are allowed.
+ var proto string
+ switch header.TransportProtocol {
+ case TCPv4:
+ proto = "TCP4"
+ case TCPv6:
+ proto = "TCP6"
+ default:
+ // Unknown connection (short form)
+ return []byte("PROXY UNKNOWN" + crlf), nil
+ }
+
+ sourceAddr, sourceOK := header.SourceAddr.(*net.TCPAddr)
+ destAddr, destOK := header.DestinationAddr.(*net.TCPAddr)
+ if !sourceOK || !destOK {
+ return nil, ErrInvalidAddress
+ }
+
+ sourceIP, destIP := sourceAddr.IP, destAddr.IP
+ switch header.TransportProtocol {
+ case TCPv4:
+ sourceIP = sourceIP.To4()
+ destIP = destIP.To4()
+ case TCPv6:
+ sourceIP = sourceIP.To16()
+ destIP = destIP.To16()
+ }
+ if sourceIP == nil || destIP == nil {
+ return nil, ErrInvalidAddress
+ }
+
+ buf := bytes.NewBuffer(make([]byte, 0, 108))
+ buf.Write(SIGV1)
+ buf.WriteString(separator)
+ buf.WriteString(proto)
+ buf.WriteString(separator)
+ buf.WriteString(sourceIP.String())
+ buf.WriteString(separator)
+ buf.WriteString(destIP.String())
+ buf.WriteString(separator)
+ buf.WriteString(strconv.Itoa(sourceAddr.Port))
+ buf.WriteString(separator)
+ buf.WriteString(strconv.Itoa(destAddr.Port))
+ buf.WriteString(crlf)
+
+ return buf.Bytes(), nil
+}
+
+func parseV1PortNumber(portStr string) (int, error) {
+ port, err := strconv.Atoi(portStr)
+ if err != nil {
+ return 0, fmt.Errorf("%w: %w", ErrInvalidPortNumber, err)
+ }
+ if port < 0 || port > 65535 {
+ return 0, ErrInvalidPortNumber
+ }
+ return port, nil
+}
+
+func parseV1IPAddress(protocol AddressFamilyAndProtocol, addrStr string) (net.IP, error) {
+ addr, err := netip.ParseAddr(addrStr)
+ if err != nil {
+ return nil, fmt.Errorf("%w: %w", ErrInvalidAddress, err)
+ }
+
+ switch protocol {
+ case TCPv4:
+ if addr.Is4() {
+ return net.IP(addr.AsSlice()), nil
+ }
+ case TCPv6:
+ // Some proxies (notably nginx OSS stream module) emit plain IPv4
+ // addresses in TCP6 headers when the backend is IPv4 but the client
+ // is IPv6. Promote to IPv4-mapped IPv6 for interoperability.
+ //
+ // This is an intentional departure from the PROXY protocol v1 spec,
+ // which states that addresses in a TCP6 line must be in IPv6 format.
+ if addr.Is6() || addr.Is4In6() {
+ return net.IP(addr.AsSlice()), nil
+ }
+ // ATTENTION: this is a lossy conversion — round-trip serialization will
+ // render the address as "::ffff:x.x.x.x" rather than the original "x.x.x.x".
+ if addr.Is4() {
+ mapped := netip.AddrFrom16(addr.As16())
+ return net.IP(mapped.AsSlice()), nil
+ }
+ }
+
+ return nil, ErrInvalidAddress
+}
diff --git a/vendor/github.com/pires/go-proxyproto/v2.go b/vendor/github.com/pires/go-proxyproto/v2.go
new file mode 100644
index 0000000000..432bcc583f
--- /dev/null
+++ b/vendor/github.com/pires/go-proxyproto/v2.go
@@ -0,0 +1,305 @@
+package proxyproto
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "math"
+ "net"
+)
+
+// maxV2HeaderSize is the maximum acceptable size of a V2 header.
+//
+// A V2 header may be at most 16 bytes + 64KiB large. We enforce a lower limit
+// to mitigate memory allocation DoS while allowing real-world legitimate
+// headers. PP2_SUBTYPE_SSL_CLIENT_CERT is typically between 1 and 2KiB, so we
+// use a 4KiB limit to leave some room for other TLVs.
+const maxV2HeaderSize = 4096
+
+var (
+ lengthUnspec = uint16(0)
+ lengthV4 = uint16(12)
+ lengthV6 = uint16(36)
+ lengthUnix = uint16(216)
+ lengthUnspecBytes = func() []byte {
+ a := make([]byte, 2)
+ binary.BigEndian.PutUint16(a, lengthUnspec)
+ return a
+ }()
+ lengthV4Bytes = func() []byte {
+ a := make([]byte, 2)
+ binary.BigEndian.PutUint16(a, lengthV4)
+ return a
+ }()
+ lengthV6Bytes = func() []byte {
+ a := make([]byte, 2)
+ binary.BigEndian.PutUint16(a, lengthV6)
+ return a
+ }()
+ lengthUnixBytes = func() []byte {
+ a := make([]byte, 2)
+ binary.BigEndian.PutUint16(a, lengthUnix)
+ return a
+ }()
+ errUint16Overflow = errors.New("proxyproto: uint16 overflow")
+)
+
+type _ports struct {
+ SrcPort uint16
+ DstPort uint16
+}
+
+type _addr4 struct {
+ Src [4]byte
+ Dst [4]byte
+ SrcPort uint16
+ DstPort uint16
+}
+
+type _addr6 struct {
+ Src [16]byte
+ Dst [16]byte
+ _ports
+}
+
+type _addrUnix struct {
+ Src [108]byte
+ Dst [108]byte
+}
+
+func parseVersion2(reader *bufio.Reader) (header *Header, err error) {
+ // Skip first 12 bytes (signature)
+ for range 12 {
+ if _, err = reader.ReadByte(); err != nil {
+ return nil, fmt.Errorf("%w: %w", ErrCantReadProtocolVersionAndCommand, err)
+ }
+ }
+
+ header = new(Header)
+ header.Version = 2
+
+ // Read the 13th byte, protocol version and command
+ b13, err := reader.ReadByte()
+ if err != nil {
+ return nil, fmt.Errorf("%w: %w", ErrCantReadProtocolVersionAndCommand, err)
+ }
+ header.Command = ProtocolVersionAndCommand(b13)
+ if _, ok := supportedCommand[header.Command]; !ok {
+ return nil, ErrUnsupportedProtocolVersionAndCommand
+ }
+
+ // Read the 14th byte, address family and protocol
+ b14, err := reader.ReadByte()
+ if err != nil {
+ return nil, fmt.Errorf("%w: %w", ErrCantReadAddressFamilyAndProtocol, err)
+ }
+ header.TransportProtocol = AddressFamilyAndProtocol(b14)
+ // UNSPEC is only supported when LOCAL is set.
+ if header.TransportProtocol == UNSPEC && header.Command != LOCAL {
+ return nil, ErrUnsupportedAddressFamilyAndProtocol
+ }
+
+ // Make sure there are bytes available as specified in length
+ var length uint16
+ if err := binary.Read(reader, binary.BigEndian, &length); err != nil {
+ return nil, fmt.Errorf("%w: %w", ErrCantReadLength, err)
+ }
+ if !header.validateLength(length) {
+ return nil, ErrInvalidLength
+ }
+
+ // Return early if the length is zero, which means that
+ // there's no address information and TLVs present for UNSPEC.
+ if length == 0 {
+ return header, nil
+ }
+
+ if length > maxV2HeaderSize {
+ return nil, ErrInvalidLength
+ }
+
+ // Length-limited reader for payload section
+ payloadReader := io.LimitReader(reader, int64(length)).(*io.LimitedReader)
+
+ // Read addresses and ports for protocols other than UNSPEC.
+ // Ignore address information for UNSPEC, and skip straight to read TLVs,
+ // since the length is greater than zero.
+ if header.TransportProtocol != UNSPEC {
+ if header.TransportProtocol.IsIPv4() {
+ var addr _addr4
+ if err := binary.Read(payloadReader, binary.BigEndian, &addr); err != nil {
+ return nil, fmt.Errorf("%w: %w", ErrInvalidAddress, err)
+ }
+ header.SourceAddr = newIPAddr(header.TransportProtocol, addr.Src[:], addr.SrcPort)
+ header.DestinationAddr = newIPAddr(header.TransportProtocol, addr.Dst[:], addr.DstPort)
+ } else if header.TransportProtocol.IsIPv6() {
+ var addr _addr6
+ if err := binary.Read(payloadReader, binary.BigEndian, &addr); err != nil {
+ return nil, fmt.Errorf("%w: %w", ErrInvalidAddress, err)
+ }
+ header.SourceAddr = newIPAddr(header.TransportProtocol, addr.Src[:], addr.SrcPort)
+ header.DestinationAddr = newIPAddr(header.TransportProtocol, addr.Dst[:], addr.DstPort)
+ } else if header.TransportProtocol.IsUnix() {
+ var addr _addrUnix
+ if err := binary.Read(payloadReader, binary.BigEndian, &addr); err != nil {
+ return nil, fmt.Errorf("%w: %w", ErrInvalidAddress, err)
+ }
+
+ network := "unix"
+ if header.TransportProtocol.IsDatagram() {
+ network = "unixgram"
+ }
+
+ header.SourceAddr = &net.UnixAddr{
+ Net: network,
+ Name: parseUnixName(addr.Src[:]),
+ }
+ header.DestinationAddr = &net.UnixAddr{
+ Net: network,
+ Name: parseUnixName(addr.Dst[:]),
+ }
+ }
+ }
+
+ // Copy bytes for optional Type-Length-Value vector
+ header.rawTLVs = make([]byte, payloadReader.N) // Allocate minimum size slice
+ if _, err = io.ReadFull(payloadReader, header.rawTLVs); err != nil && err != io.EOF {
+ return nil, err
+ }
+
+ if payloadReader.N != 0 {
+ return nil, ErrInvalidLength
+ }
+
+ return header, nil
+}
+
+func (header *Header) formatVersion2() ([]byte, error) {
+ var buf bytes.Buffer
+ buf.Write(SIGV2)
+ buf.WriteByte(header.Command.toByte())
+ buf.WriteByte(header.TransportProtocol.toByte())
+ if header.TransportProtocol.IsUnspec() {
+ // For UNSPEC, write no addresses and ports but only TLVs if they are present
+ hdrLen, err := addTLVLen(lengthUnspecBytes, len(header.rawTLVs))
+ if err != nil {
+ return nil, err
+ }
+ buf.Write(hdrLen)
+ } else {
+ var addrSrc, addrDst []byte
+ if header.TransportProtocol.IsIPv4() {
+ hdrLen, err := addTLVLen(lengthV4Bytes, len(header.rawTLVs))
+ if err != nil {
+ return nil, err
+ }
+ buf.Write(hdrLen)
+ sourceIP, destIP, _ := header.IPs()
+ addrSrc = sourceIP.To4()
+ addrDst = destIP.To4()
+ } else if header.TransportProtocol.IsIPv6() {
+ hdrLen, err := addTLVLen(lengthV6Bytes, len(header.rawTLVs))
+ if err != nil {
+ return nil, err
+ }
+ buf.Write(hdrLen)
+ sourceIP, destIP, _ := header.IPs()
+ addrSrc = sourceIP.To16()
+ addrDst = destIP.To16()
+ } else if header.TransportProtocol.IsUnix() {
+ buf.Write(lengthUnixBytes)
+ sourceAddr, destAddr, ok := header.UnixAddrs()
+ if !ok {
+ return nil, ErrInvalidAddress
+ }
+ addrSrc = formatUnixName(sourceAddr.Name)
+ addrDst = formatUnixName(destAddr.Name)
+ }
+
+ if addrSrc == nil || addrDst == nil {
+ return nil, ErrInvalidAddress
+ }
+ buf.Write(addrSrc)
+ buf.Write(addrDst)
+
+ if sourcePort, destPort, ok := header.Ports(); ok {
+ if sourcePort < 0 || sourcePort > math.MaxUint16 || destPort < 0 || destPort > math.MaxUint16 {
+ return nil, ErrInvalidPortNumber
+ }
+ portBytes := make([]byte, 2)
+
+ //nolint:gosec // Bounds are checked above.
+ binary.BigEndian.PutUint16(portBytes, uint16(sourcePort))
+ buf.Write(portBytes)
+
+ //nolint:gosec // Bounds are checked above.
+ binary.BigEndian.PutUint16(portBytes, uint16(destPort))
+ buf.Write(portBytes)
+ }
+ }
+
+ if len(header.rawTLVs) > 0 {
+ buf.Write(header.rawTLVs)
+ }
+
+ return buf.Bytes(), nil
+}
+
+func (header *Header) validateLength(length uint16) bool {
+ if header.TransportProtocol.IsIPv4() {
+ return length >= lengthV4
+ } else if header.TransportProtocol.IsIPv6() {
+ return length >= lengthV6
+ } else if header.TransportProtocol.IsUnix() {
+ return length >= lengthUnix
+ } else if header.TransportProtocol.IsUnspec() {
+ return length >= lengthUnspec
+ }
+ return false
+}
+
+// addTLVLen adds the length of the TLV to the header length or errors on uint16 overflow.
+func addTLVLen(cur []byte, tlvLen int) ([]byte, error) {
+ if tlvLen == 0 {
+ return cur, nil
+ }
+ curLen := binary.BigEndian.Uint16(cur)
+ newLen := int(curLen) + tlvLen
+ if newLen >= 1<<16 {
+ return nil, errUint16Overflow
+ }
+ a := make([]byte, 2)
+ //nolint:gosec // newLen bounds are validated above.
+ binary.BigEndian.PutUint16(a, uint16(newLen))
+ return a, nil
+}
+
+func newIPAddr(transport AddressFamilyAndProtocol, ip net.IP, port uint16) net.Addr {
+ if transport.IsStream() {
+ return &net.TCPAddr{IP: ip, Port: int(port)}
+ }
+ if transport.IsDatagram() {
+ return &net.UDPAddr{IP: ip, Port: int(port)}
+ }
+ return nil
+}
+
+func parseUnixName(b []byte) string {
+ before, _, ok := bytes.Cut(b, []byte{0})
+ if !ok {
+ return string(b)
+ }
+ return string(before)
+}
+
+func formatUnixName(name string) []byte {
+ n := int(lengthUnix) / 2
+ if len(name) >= n {
+ return []byte(name[:n])
+ }
+ pad := make([]byte, n-len(name))
+ return append([]byte(name), pad...)
+}
diff --git a/vendor/github.com/pires/go-proxyproto/version_cmd.go b/vendor/github.com/pires/go-proxyproto/version_cmd.go
new file mode 100644
index 0000000000..e80681c315
--- /dev/null
+++ b/vendor/github.com/pires/go-proxyproto/version_cmd.go
@@ -0,0 +1,48 @@
+package proxyproto
+
+// ProtocolVersionAndCommand represents the command in proxy protocol v2.
+// Command doesn't exist in v1 but it should be set since other parts of
+// this library may rely on it for determining connection details.
+type ProtocolVersionAndCommand byte
+
+const (
+ // LOCAL represents the LOCAL command in v2 or UNKNOWN transport in v1,
+ // in which case no address information is expected.
+ LOCAL ProtocolVersionAndCommand = '\x20'
+ // PROXY represents the PROXY command in v2 or transport is not UNKNOWN in v1,
+ // in which case valid local/remote address and port information is expected.
+ PROXY ProtocolVersionAndCommand = '\x21'
+)
+
+var supportedCommand = map[ProtocolVersionAndCommand]bool{
+ LOCAL: true,
+ PROXY: true,
+}
+
+// IsLocal returns true if the command in v2 is LOCAL or the transport in v1 is UNKNOWN,
+// i.e. when no address information is expected, false otherwise.
+func (pvc ProtocolVersionAndCommand) IsLocal() bool {
+ return LOCAL == pvc
+}
+
+// IsProxy returns true if the command in v2 is PROXY or the transport in v1 is not UNKNOWN,
+// i.e. when valid local/remote address and port information is expected, false otherwise.
+func (pvc ProtocolVersionAndCommand) IsProxy() bool {
+ return PROXY == pvc
+}
+
+// IsUnspec returns true if the command is unspecified, false otherwise.
+func (pvc ProtocolVersionAndCommand) IsUnspec() bool {
+ // Must be LOCAL or PROXY.
+ return !pvc.IsLocal() && !pvc.IsProxy()
+}
+
+func (pvc ProtocolVersionAndCommand) toByte() byte {
+ if pvc.IsLocal() {
+ return byte(LOCAL)
+ } else if pvc.IsProxy() {
+ return byte(PROXY)
+ }
+
+ return byte(LOCAL)
+}
diff --git a/vendor/google.golang.org/grpc/clientconn.go b/vendor/google.golang.org/grpc/clientconn.go
index c4bca5203e..b27c7e84a3 100644
--- a/vendor/google.golang.org/grpc/clientconn.go
+++ b/vendor/google.golang.org/grpc/clientconn.go
@@ -24,12 +24,10 @@ import (
"fmt"
"math"
"net/url"
- "os"
"slices"
"strings"
"sync"
"sync/atomic"
- "syscall"
"time"
"google.golang.org/grpc/balancer"
@@ -1573,26 +1571,13 @@ func (ac *addrConn) createTransport(ctx context.Context, addr resolver.Address,
// to the provided transport.GoAwayInfo, as specified by gRFC A94:
// https://github.com/grpc/proposal/blob/master/A94-grpc-subchannel-disconnections-metrics.md
func disconnectErrorString(info transport.GoAwayInfo) string {
- err := info.Err
- var sysErr syscall.Errno
- switch {
- case info.Reason != transport.GoAwayInvalid:
+ if info.Reason != transport.GoAwayInvalid {
return fmt.Sprintf("GOAWAY %s", info.GoAwayCode.String())
- case err == nil:
- return "unknown"
- case errors.Is(err, context.Canceled):
- return "subchannel shutdown"
- case errors.Is(err, syscall.ECONNRESET):
- return "connection reset"
- case errors.Is(err, syscall.ETIMEDOUT), errors.Is(err, context.DeadlineExceeded), errors.Is(err, os.ErrDeadlineExceeded):
- return "connection timed out"
- case errors.Is(err, syscall.ECONNABORTED):
- return "connection aborted"
- case errors.As(err, &sysErr):
- return "socket error"
- default:
+ }
+ if info.Err == nil {
return "unknown"
}
+ return disconnectErrorLabel(info.Err)
}
// startHealthCheck starts the health checking stream (RPC) to watch the health
diff --git a/vendor/google.golang.org/grpc/clientconn_disconnect_reason_noplan9.go b/vendor/google.golang.org/grpc/clientconn_disconnect_reason_noplan9.go
new file mode 100644
index 0000000000..f0fcd88423
--- /dev/null
+++ b/vendor/google.golang.org/grpc/clientconn_disconnect_reason_noplan9.go
@@ -0,0 +1,48 @@
+//go:build !plan9
+
+/*
+ *
+ * Copyright 2026 gRPC authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package grpc
+
+import (
+ "context"
+ "errors"
+ "os"
+ "syscall"
+)
+
+// disconnectErrorLabel returns the grpc.disconnect_error metric label for a
+// transport error, as specified by gRFC A94.
+func disconnectErrorLabel(err error) string {
+ var sysErr syscall.Errno
+ switch {
+ case errors.Is(err, context.Canceled):
+ return "subchannel shutdown"
+ case errors.Is(err, syscall.ECONNRESET):
+ return "connection reset"
+ case errors.Is(err, syscall.ETIMEDOUT), errors.Is(err, context.DeadlineExceeded), errors.Is(err, os.ErrDeadlineExceeded):
+ return "connection timed out"
+ case errors.Is(err, syscall.ECONNABORTED):
+ return "connection aborted"
+ case errors.As(err, &sysErr):
+ return "socket error"
+ default:
+ return "unknown"
+ }
+}
diff --git a/vendor/google.golang.org/grpc/clientconn_disconnect_reason_plan9.go b/vendor/google.golang.org/grpc/clientconn_disconnect_reason_plan9.go
new file mode 100644
index 0000000000..930b12664c
--- /dev/null
+++ b/vendor/google.golang.org/grpc/clientconn_disconnect_reason_plan9.go
@@ -0,0 +1,39 @@
+/*
+ *
+ * Copyright 2026 gRPC authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package grpc
+
+import (
+ "context"
+ "errors"
+ "os"
+)
+
+// disconnectErrorLabel returns the grpc.disconnect_error metric label for a
+// transport error, as specified by gRFC A94. syscall.Errno does not exist on
+// plan9, so only the portable classifications are available.
+func disconnectErrorLabel(err error) string {
+ switch {
+ case errors.Is(err, context.Canceled):
+ return "subchannel shutdown"
+ case errors.Is(err, context.DeadlineExceeded), errors.Is(err, os.ErrDeadlineExceeded):
+ return "connection timed out"
+ default:
+ return "unknown"
+ }
+}
diff --git a/vendor/google.golang.org/grpc/internal/envconfig/xds.go b/vendor/google.golang.org/grpc/internal/envconfig/xds.go
index a2312f8eac..e4b6919138 100644
--- a/vendor/google.golang.org/grpc/internal/envconfig/xds.go
+++ b/vendor/google.golang.org/grpc/internal/envconfig/xds.go
@@ -69,9 +69,8 @@ var (
// https://github.com/grpc/proposal/blob/master/A87-mtls-spiffe-support.md
XDSSPIFFEEnabled = boolFromEnv("GRPC_EXPERIMENTAL_XDS_MTLS_SPIFFE", false)
- // XDSHTTPConnectEnabled is true if gRPC should parse custom Metadata
- // configuring use of an HTTP CONNECT proxy via xDS from cluster resources.
- // For more details, see:
+ // XDSHTTPConnectEnabled controls support for dynamic HTTP CONNECT proxying
+ // configured via the xDS control plane. For more details, see:
// https://github.com/grpc/proposal/blob/master/A86-xds-http-connect.md
XDSHTTPConnectEnabled = boolFromEnv("GRPC_EXPERIMENTAL_XDS_HTTP_CONNECT", false)
@@ -88,7 +87,7 @@ var (
// XDSORCAToLRSPropEnabled controls whether ORCA metrics are explicitly
// filtered and prefix-propagated to the LRS server. For more details, see:
// https://github.com/grpc/proposal/blob/master/A85-lrs-custom-metrics-changes.md
- XDSORCAToLRSPropEnabled = boolFromEnv("GRPC_EXPERIMENTAL_XDS_ORCA_LRS_PROPAGATION", false)
+ XDSORCAToLRSPropEnabled = boolFromEnv("GRPC_EXPERIMENTAL_XDS_ORCA_LRS_PROPAGATION", true)
// XDSClientExtProcEnabled indicates whether ExtProc filter is enabled on
// the client side. For more details, see:
diff --git a/vendor/google.golang.org/grpc/internal/grpcsync/callback_serializer.go b/vendor/google.golang.org/grpc/internal/grpcsync/callback_serializer.go
index 9b6d8a1fa3..d4999fcca8 100644
--- a/vendor/google.golang.org/grpc/internal/grpcsync/callback_serializer.go
+++ b/vendor/google.golang.org/grpc/internal/grpcsync/callback_serializer.go
@@ -20,10 +20,15 @@ package grpcsync
import (
"context"
+ "errors"
"google.golang.org/grpc/internal/buffer"
)
+// ErrSerializerClosed is returned by ScheduleAndWait if the CallbackSerializer
+// was closed before the callback could be scheduled.
+var ErrSerializerClosed = errors.New("callback serializer is closed")
+
// CallbackSerializer provides a mechanism to schedule callbacks in a
// synchronized manner. It provides a FIFO guarantee on the order of execution
// of scheduled callbacks. New callbacks can be scheduled by invoking the
@@ -77,6 +82,27 @@ func (cs *CallbackSerializer) ScheduleOr(f func(ctx context.Context), onFailure
}
}
+// ScheduleAndWait schedules the provided callback function f to be executed in
+// the order it was added and blocks until f has run. If the context passed to
+// NewCallbackSerializer was canceled before this method is called, f is not run
+// and ScheduleAndWait returns ErrSerializerClosed.
+//
+// Callbacks are expected to honor the context when performing any blocking
+// operations, and should return early when the context is canceled.
+func (cs *CallbackSerializer) ScheduleAndWait(f func(ctx context.Context)) error {
+ done := make(chan struct{})
+ var err error
+ cs.ScheduleOr(func(ctx context.Context) {
+ f(ctx)
+ close(done)
+ }, func() {
+ err = ErrSerializerClosed
+ close(done)
+ })
+ <-done
+ return err
+}
+
func (cs *CallbackSerializer) run(ctx context.Context) {
defer close(cs.done)
diff --git a/vendor/google.golang.org/grpc/internal/resolver/config_selector.go b/vendor/google.golang.org/grpc/internal/resolver/config_selector.go
index 6320e9b576..238950bbbf 100644
--- a/vendor/google.golang.org/grpc/internal/resolver/config_selector.go
+++ b/vendor/google.golang.org/grpc/internal/resolver/config_selector.go
@@ -24,7 +24,6 @@ import (
"sync"
"google.golang.org/grpc/internal/serviceconfig"
- "google.golang.org/grpc/metadata"
"google.golang.org/grpc/resolver"
)
@@ -52,82 +51,7 @@ type RPCConfig struct {
Context context.Context
MethodConfig serviceconfig.MethodConfig // configuration to use for this RPC
OnCommitted func() // Called when the RPC has been committed (retries no longer possible)
- Interceptor ClientInterceptor
-}
-
-// ClientStream is the same as grpc.ClientStream, but defined here for circular
-// dependency reasons.
-type ClientStream interface {
- // Header returns the header metadata received from the server if there
- // is any. It blocks if the metadata is not ready to read.
- Header() (metadata.MD, error)
- // Trailer returns the trailer metadata from the server, if there is any.
- // It must only be called after stream.CloseAndRecv has returned, or
- // stream.Recv has returned a non-nil error (including io.EOF).
- Trailer() metadata.MD
- // CloseSend closes the send direction of the stream. It closes the stream
- // when non-nil error is met. It is also not safe to call CloseSend
- // concurrently with SendMsg.
- CloseSend() error
- // Context returns the context for this stream.
- //
- // It should not be called until after Header or RecvMsg has returned. Once
- // called, subsequent client-side retries are disabled.
- Context() context.Context
- // SendMsg is generally called by generated code. On error, SendMsg aborts
- // the stream. If the error was generated by the client, the status is
- // returned directly; otherwise, io.EOF is returned and the status of
- // the stream may be discovered using RecvMsg.
- //
- // SendMsg blocks until:
- // - There is sufficient flow control to schedule m with the transport, or
- // - The stream is done, or
- // - The stream breaks.
- //
- // SendMsg does not wait until the message is received by the server. An
- // untimely stream closure may result in lost messages. To ensure delivery,
- // users should ensure the RPC completed successfully using RecvMsg.
- //
- // It is safe to have a goroutine calling SendMsg and another goroutine
- // calling RecvMsg on the same stream at the same time, but it is not safe
- // to call SendMsg on the same stream in different goroutines. It is also
- // not safe to call CloseSend concurrently with SendMsg.
- SendMsg(m any) error
- // RecvMsg blocks until it receives a message into m or the stream is
- // done. It returns io.EOF when the stream completes successfully. On
- // any other error, the stream is aborted and the error contains the RPC
- // status.
- //
- // It is safe to have a goroutine calling SendMsg and another goroutine
- // calling RecvMsg on the same stream at the same time, but it is not
- // safe to call RecvMsg on the same stream in different goroutines.
- RecvMsg(m any) error
-}
-
-// ClientInterceptor is an interceptor for gRPC client streams.
-type ClientInterceptor interface {
- // NewStream creates a ClientStream for an RPC.
- //
- // Implementations must delegate stream creation to the provided newStream
- // function. To intercept or override stream behavior, implementations
- // may wrap the ClientStream returned by the delegate.
- //
- // Note: RPCInfo.Context is currently unused and will be nil.
- //
- // The done function is invoked when the RPC has finished using its
- // underlying connection or if a connection could not be assigned. Because
- // interceptors operate at the application layer, RPC operations may
- // continue on the ClientStream even after done has been called. The
- // caller must ensure done is non-nil.
- //
- // To ensure RPC completion notifications propagate through the entire
- // interceptor chain, implementations must ensure that the done function
- // passed to the delegate newStream invokes the done function passed to
- // NewStream.
- NewStream(ctx context.Context, ri RPCInfo, done func(), newStream func(ctx context.Context, done func()) (ClientStream, error)) (ClientStream, error)
- // Close closes the interceptor. Once called, no new calls to NewStream are
- // accepted. Ongoing calls to NewStream are allowed to complete.
- Close()
+ Interceptor any
}
// ServerInterceptor is an interceptor for incoming RPC's on gRPC server side.
diff --git a/vendor/google.golang.org/grpc/internal/transport/client_stream.go b/vendor/google.golang.org/grpc/internal/transport/client_stream.go
index ad382b0fda..046f0a5557 100644
--- a/vendor/google.golang.org/grpc/internal/transport/client_stream.go
+++ b/vendor/google.golang.org/grpc/internal/transport/client_stream.go
@@ -39,9 +39,8 @@ const nonGRPCDataMaxLen = 1024
type ClientStream struct {
Stream // Embed for common stream functionality.
- ct *http2Client
- done chan struct{} // closed at the end of stream to unblock writers.
- doneFunc func() // invoked at the end of stream.
+ ct *http2Client
+ done chan struct{} // closed at the end of stream to unblock writers.
headerChan chan struct{} // closed to indicate the end of header metadata.
header metadata.MD // the received header metadata
diff --git a/vendor/google.golang.org/grpc/internal/transport/http2_client.go b/vendor/google.golang.org/grpc/internal/transport/http2_client.go
index 822c09ba62..c19b45080e 100644
--- a/vendor/google.golang.org/grpc/internal/transport/http2_client.go
+++ b/vendor/google.golang.org/grpc/internal/transport/http2_client.go
@@ -498,7 +498,6 @@ func (t *http2Client) newStream(ctx context.Context, callHdr *CallHdr, handler s
ct: t,
done: make(chan struct{}),
headerChan: make(chan struct{}),
- doneFunc: callHdr.DoneFunc,
statsHandler: handler,
}
s.Stream.buf.init()
@@ -998,9 +997,6 @@ func (t *http2Client) closeStream(s *ClientStream, err error, rst bool, rstCode
t.controlBuf.executeAndPut(addBackStreamQuota, cleanup)
// This will unblock write.
close(s.done)
- if s.doneFunc != nil {
- s.doneFunc()
- }
}
// Close kicks off the shutdown process of the transport. This should be called
diff --git a/vendor/google.golang.org/grpc/internal/transport/transport.go b/vendor/google.golang.org/grpc/internal/transport/transport.go
index 6dfae39849..d2e49538f0 100644
--- a/vendor/google.golang.org/grpc/internal/transport/transport.go
+++ b/vendor/google.golang.org/grpc/internal/transport/transport.go
@@ -594,8 +594,6 @@ type CallHdr struct {
PreviousAttempts int // value of grpc-previous-rpc-attempts header to set
- DoneFunc func() // called when the stream is finished
-
// Authority is used to explicitly override the `:authority` header.
//
// This value comes from one of two sources:
diff --git a/vendor/google.golang.org/grpc/stream.go b/vendor/google.golang.org/grpc/stream.go
index 4aac644a83..51aff85dfb 100644
--- a/vendor/google.golang.org/grpc/stream.go
+++ b/vendor/google.golang.org/grpc/stream.go
@@ -201,6 +201,15 @@ func endOfClientStream(cc *ClientConn, err error, opts ...CallOption) {
}
}
+// clientInterceptor is structurally identical to the ClientInterceptor defined
+// in internal/xds/httpfilter/httpfilter.go. It is defined locally here so that
+// we can type-assert the generic Interceptor field in iresolver.RPCConfig
+// without introducing a dependency on xDS packages.
+type clientInterceptor interface {
+ NewStream(ctx context.Context, ri iresolver.RPCInfo, newStream func(ctx context.Context, opts ...CallOption) (ClientStream, error), opts ...CallOption) (ClientStream, error)
+ Close()
+}
+
func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, opts ...CallOption) (_ ClientStream, err error) {
if channelz.IsOn() {
cc.incrCallsStarted()
@@ -244,8 +253,11 @@ func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, meth
mc := &emptyMethodConfig
var onCommit func()
- newStream := func(ctx context.Context, done func()) (iresolver.ClientStream, error) {
- return newClientStreamWithParams(ctx, desc, cc, method, mc, onCommit, done, nameResolutionDelayed, opts...)
+ newStream := func(ctx context.Context, filterOpts ...CallOption) (ClientStream, error) {
+ if filterOpts != nil {
+ opts = combine(opts, filterOpts)
+ }
+ return newClientStreamWithParams(ctx, desc, cc, method, mc, onCommit, nameResolutionDelayed, opts...)
}
rpcInfo := iresolver.RPCInfo{Context: ctx, Method: method}
@@ -270,20 +282,24 @@ func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, meth
if rpcConfig.Interceptor != nil {
rpcInfo.Context = nil
ns := newStream
- newStream = func(ctx context.Context, done func()) (iresolver.ClientStream, error) {
- cs, err := rpcConfig.Interceptor.NewStream(ctx, rpcInfo, done, ns)
- if err != nil {
- return nil, toRPCErr(err)
+ if interceptor, ok := rpcConfig.Interceptor.(clientInterceptor); ok {
+ newStream = func(ctx context.Context, filterOpts ...CallOption) (ClientStream, error) {
+ cs, err := interceptor.NewStream(ctx, rpcInfo, ns, filterOpts...)
+ if err != nil {
+ return nil, toRPCErr(err)
+ }
+ return cs, nil
}
- return cs, nil
+ } else {
+ return nil, status.Errorf(codes.Internal, "invalid client interceptor type %T", rpcConfig.Interceptor)
}
}
}
- return newStream(ctx, func() {})
+ return newStream(ctx)
}
-func newClientStreamWithParams(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, mc *serviceconfig.MethodConfig, onCommit, doneFunc func(), nameResolutionDelayed bool, opts ...CallOption) (_ iresolver.ClientStream, err error) {
+func newClientStreamWithParams(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, mc *serviceconfig.MethodConfig, onCommit func(), nameResolutionDelayed bool, opts ...CallOption) (_ ClientStream, err error) {
callInfo := defaultCallInfo()
if mc.WaitForReady != nil {
callInfo.failFast = !*mc.WaitForReady
@@ -321,7 +337,6 @@ func newClientStreamWithParams(ctx context.Context, desc *StreamDesc, cc *Client
Host: cc.authority,
Method: method,
ContentSubtype: callInfo.contentSubtype,
- DoneFunc: doneFunc,
Authority: callInfo.authority,
}
if allowed := callInfo.acceptedResponseCompressors; len(allowed) > 0 {
diff --git a/vendor/google.golang.org/grpc/version.go b/vendor/google.golang.org/grpc/version.go
index 53c737feeb..4083c03908 100644
--- a/vendor/google.golang.org/grpc/version.go
+++ b/vendor/google.golang.org/grpc/version.go
@@ -19,4 +19,4 @@
package grpc
// Version is the current grpc version.
-const Version = "1.82.1"
+const Version = "1.83.0"
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 99061d270f..af097458b5 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -14,8 +14,11 @@ charm.land/lipgloss/v2
# charm.land/log/v2 v2.0.0
## explicit; go 1.25.8
charm.land/log/v2
-# charm.land/wish/v2 v2.0.1
-## explicit; go 1.25.9
+# charm.land/ssh v0.4.2
+## explicit; go 1.25.0
+charm.land/ssh
+# charm.land/wish/v2 v2.0.3
+## explicit; go 1.25.12
charm.land/wish/v2
charm.land/wish/v2/activeterm
charm.land/wish/v2/bubbletea
@@ -54,12 +57,9 @@ github.com/charmbracelet/ultraviolet
github.com/charmbracelet/x/ansi
github.com/charmbracelet/x/ansi/kitty
github.com/charmbracelet/x/ansi/parser
-# github.com/charmbracelet/x/conpty v0.1.1
+# github.com/charmbracelet/x/conpty v0.2.0
## explicit; go 1.24.0
github.com/charmbracelet/x/conpty
-# github.com/charmbracelet/x/errors v0.0.0-20251110184232-6ab307057ac7
-## explicit; go 1.24.0
-github.com/charmbracelet/x/errors
# github.com/charmbracelet/x/term v0.2.2
## explicit; go 1.24.0
github.com/charmbracelet/x/term
@@ -69,6 +69,9 @@ github.com/charmbracelet/x/termios
# github.com/charmbracelet/x/windows v0.2.2
## explicit; go 1.23.0
github.com/charmbracelet/x/windows
+# github.com/charmbracelet/x/xpty v0.1.4
+## explicit; go 1.25.0
+github.com/charmbracelet/x/xpty
# github.com/cilium/ebpf v0.22.0
## explicit; go 1.25.0
github.com/cilium/ebpf
@@ -261,6 +264,9 @@ github.com/pierrec/lz4/v4/internal/lz4block
github.com/pierrec/lz4/v4/internal/lz4errors
github.com/pierrec/lz4/v4/internal/lz4stream
github.com/pierrec/lz4/v4/internal/xxh32
+# github.com/pires/go-proxyproto v0.12.0
+## explicit; go 1.25
+github.com/pires/go-proxyproto
# github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
## explicit
github.com/pmezard/go-difflib/difflib
@@ -384,8 +390,8 @@ golang.org/x/crypto/ssh/knownhosts
# golang.org/x/crypto/x509roots/fallback v0.0.0-20260630172432-7626c5025624
## explicit; go 1.25.0
golang.org/x/crypto/x509roots/fallback/bundle
-# golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
-## explicit; go 1.24.0
+# golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
+## explicit; go 1.25.0
golang.org/x/exp/slices
golang.org/x/exp/slog
golang.org/x/exp/slog/internal
@@ -482,10 +488,10 @@ golang.zx2c4.com/wireguard/wgctrl/internal/wguser
golang.zx2c4.com/wireguard/wgctrl/internal/wgwindows
golang.zx2c4.com/wireguard/wgctrl/internal/wgwindows/internal/ioctl
golang.zx2c4.com/wireguard/wgctrl/wgtypes
-# google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478
+# google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa
## explicit; go 1.25.0
google.golang.org/genproto/googleapis/rpc/status
-# google.golang.org/grpc v1.82.1
+# google.golang.org/grpc v1.83.0
## explicit; go 1.25.0
google.golang.org/grpc
google.golang.org/grpc/attributes