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 + +

+ Latest Release + GoDoc + Build Status + Codecov branch +

+ +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). + +The Charm logo + +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 + +[![Actions Status](https://github.com/pires/go-proxyproto/workflows/test/badge.svg)](https://github.com/pires/go-proxyproto/actions) +[![Coverage Status](https://coveralls.io/repos/github/pires/go-proxyproto/badge.svg?branch=main)](https://coveralls.io/github/pires/go-proxyproto?branch=main) +[![Go Report Card](https://goreportcard.com/badge/github.com/pires/go-proxyproto)](https://goreportcard.com/report/github.com/pires/go-proxyproto) +[![](https://godoc.org/github.com/pires/go-proxyproto?status.svg)](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