Compare commits
2 Commits
73a0e7b85a
...
a357f2a924
Author | SHA1 | Date | |
---|---|---|---|
kayos@tcp.direct | a357f2a924 | ||
kayos@tcp.direct | 42949acbaa |
20
go.mod
20
go.mod
|
@ -7,6 +7,8 @@ require (
|
|||
git.tcp.direct/kayos/common v0.7.6
|
||||
git.tcp.direct/tcp.direct/database v0.0.0-20220829103039-b85255196bd1
|
||||
github.com/amimof/huego v1.2.1
|
||||
github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103
|
||||
github.com/charmbracelet/wish v1.0.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/dhamith93/systats v0.2.0
|
||||
github.com/gliderlabs/ssh v0.3.5
|
||||
|
@ -14,18 +16,21 @@ require (
|
|||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/mazznoer/colorgrad v0.9.0
|
||||
github.com/muesli/termenv v0.12.0
|
||||
github.com/muesli/termenv v0.13.0
|
||||
github.com/rs/zerolog v1.27.0
|
||||
github.com/spf13/viper v1.12.0
|
||||
go4.org/netipx v0.0.0-20230125063823-8449b0a6169f
|
||||
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d
|
||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b
|
||||
golang.org/x/crypto v0.3.0
|
||||
golang.org/x/net v0.2.0
|
||||
)
|
||||
|
||||
require (
|
||||
git.tcp.direct/Mirrors/bitcask-mirror v0.0.0-20220228092422-1ec4297c7e34 // indirect
|
||||
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 // indirect
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
|
||||
github.com/caarlos0/sshmarshal v0.1.0 // indirect
|
||||
github.com/charmbracelet/keygen v0.3.0 // indirect
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/gofrs/flock v0.8.0 // indirect
|
||||
|
@ -34,10 +39,11 @@ require (
|
|||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-tty v0.0.4 // indirect
|
||||
github.com/mazznoer/csscolorparser v0.1.2 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
|
||||
|
@ -51,8 +57,8 @@ require (
|
|||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20200228211341-fcea875c7e85 // indirect
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/sys v0.2.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0 // indirect
|
||||
|
|
40
go.sum
40
go.sum
|
@ -64,12 +64,22 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC
|
|||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
|
||||
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
|
||||
github.com/caarlos0/sshmarshal v0.1.0 h1:zTCZrDORFfWh526Tsb7vCm3+Yg/SfW/Ub8aQDeosk0I=
|
||||
github.com/caarlos0/sshmarshal v0.1.0/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/charmbracelet/keygen v0.3.0 h1:mXpsQcH7DDlST5TddmXNXjS0L7ECk4/kLQYyBcsan2Y=
|
||||
github.com/charmbracelet/keygen v0.3.0/go.mod h1:1ukgO8806O25lUZ5s0IrNur+RlwTBERlezdgW71F5rM=
|
||||
github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103 h1:wpHMERIN0pQZE635jWwT1dISgfjbpUcEma+fbPKSMCU=
|
||||
github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103/go.mod h1:0Vm2/8yBljiLDnGJHU8ehswfawrEybGk33j5ssqKQVM=
|
||||
github.com/charmbracelet/wish v1.0.0 h1:Ca/Sm8NfbW0/hEtw+voxwgKd5iRq9v7P3X/cDVV8doY=
|
||||
github.com/charmbracelet/wish v1.0.0/go.mod h1:LatUnJh7kQXK5kvkvuwvddCSeUn8Yss02nDh54yLQas=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||
|
@ -168,7 +178,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
|
@ -261,11 +271,12 @@ github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb
|
|||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-tty v0.0.4 h1:NVikla9X8MN0SQAqCYzpGyXv0jY7MNl3HOWD2dkle7E=
|
||||
github.com/mattn/go-tty v0.0.4/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
|
@ -276,6 +287,7 @@ github.com/mazznoer/csscolorparser v0.1.2/go.mod h1:Aj22+L/rYN/Y6bj3bYqO3N6g1dtd
|
|||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
|
@ -288,8 +300,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
|
|||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc=
|
||||
github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A=
|
||||
github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
|
||||
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
|
@ -421,8 +433,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
|
||||
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
@ -502,8 +515,9 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
|
|||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
|
||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -583,15 +597,16 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -600,8 +615,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
|
|
@ -3,6 +3,7 @@ package cli
|
|||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
// "io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -33,14 +34,21 @@ func executor(cmd string) {
|
|||
var status = 0
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Error().Msgf("PANIC: %s", r)
|
||||
log.Error().Caller().Msgf("PANIC: %s", r)
|
||||
}
|
||||
if _, ok := noHist[cmd]; !ok && status == 0 {
|
||||
history = append(history, cmd)
|
||||
go saveHist()
|
||||
}
|
||||
}()
|
||||
|
||||
// hacky bugfix
|
||||
cmd = strings.ReplaceAll(cmd, "#", "_POUNDSIGN_")
|
||||
args, err := shlex.Split(strings.TrimSpace(cmd))
|
||||
for i, arg := range args {
|
||||
args[i] = strings.ReplaceAll(arg, "_POUNDSIGN_", "#")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error().Msgf("error parsing command: %s", err)
|
||||
status = 1
|
||||
|
@ -214,6 +222,7 @@ func saveHist() {
|
|||
_ = os.WriteFile(filepath.Join(pth, ".ziggs_history"), []byte(strings.Join(history, "\n")), 0644)
|
||||
}
|
||||
|
||||
// func StartCLI(r io.Reader, w io.Writer) {
|
||||
func StartCLI() {
|
||||
log = config.GetLogger()
|
||||
processBridges()
|
||||
|
@ -223,9 +232,13 @@ func StartCLI() {
|
|||
processScenes(ziggy.GetSceneMap())
|
||||
}()
|
||||
buildTime, _ := common.Version()
|
||||
|
||||
// cli.NewStdoutWriter().
|
||||
prompt = cli.New(
|
||||
executor,
|
||||
completer,
|
||||
// cli.OptionWriter(w),
|
||||
// cli.Op(r),
|
||||
// cli.OptionPrefixBackgroundColor(cli.Black),
|
||||
cli.OptionPrefixTextColor(cli.Yellow),
|
||||
cli.OptionHistory(getHist()),
|
||||
|
|
|
@ -45,7 +45,7 @@ func cpuInit(argVal string, bridge *ziggy.Bridge, cpuTarget cmdTarget) error {
|
|||
cpuOn = false
|
||||
}()
|
||||
var lights []*huego.Light
|
||||
for _, l := range cpuTarget.(*huego.Group).Lights {
|
||||
for _, l := range cpuTarget.(*ziggy.HueGroup).Lights {
|
||||
lint, _ := strconv.Atoi(l)
|
||||
lptr, err := bridge.GetLight(lint)
|
||||
if err != nil {
|
||||
|
|
|
@ -129,6 +129,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error {
|
|||
if len(args) == argHead-1 {
|
||||
return errors.New("not enough arguments")
|
||||
}
|
||||
log.Trace().Caller().Msgf("color, args: %v", args)
|
||||
argHead++
|
||||
newcolor, err := common.ParseHexColorFast(args[argHead])
|
||||
if err != nil {
|
||||
|
|
|
@ -49,6 +49,8 @@ var (
|
|||
SSHListen string
|
||||
// SSHHostKey is the path to the SSH host key, if any. If none is specified, one will be generated.
|
||||
SSHHostKey string
|
||||
// SSHPublicKeys is a list of public keys that are allowed to connect to ziggs via SSH.
|
||||
SSHPublicKeys []string
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package data
|
||||
|
||||
/*
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
|
@ -208,3 +209,4 @@ func TestAddSequence(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package ssh
|
||||
|
||||
import (
|
||||
"git.tcp.direct/kayos/common/squish"
|
||||
"github.com/charmbracelet/ssh"
|
||||
|
||||
"git.tcp.direct/kayos/ziggs/internal/config"
|
||||
)
|
||||
|
||||
func (s *Server) GetStaticKeys() map[string]ssh.PublicKey {
|
||||
if s.keys != nil {
|
||||
return s.keys
|
||||
}
|
||||
pubs := make(map[string]ssh.PublicKey)
|
||||
for _, key := range config.SSHPublicKeys {
|
||||
pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pubs[squish.B64e(pub.Marshal())] = pub
|
||||
}
|
||||
s.keys = pubs
|
||||
return pubs
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.tcp.direct/kayos/common/squish"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"git.tcp.direct/kayos/ziggs/internal/cli"
|
||||
"git.tcp.direct/kayos/ziggs/internal/config"
|
||||
|
||||
"github.com/charmbracelet/ssh"
|
||||
"github.com/charmbracelet/wish"
|
||||
"github.com/charmbracelet/wish/logging"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
*ssh.Server
|
||||
clients map[string]ssh.Session
|
||||
keys map[string]ssh.PublicKey
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
func (s *Server) checkAuth(h ssh.Handler) ssh.Handler {
|
||||
return func(ss ssh.Session) {
|
||||
k, ok := s.GetStaticKeys()[squish.B64e(ss.PublicKey().Marshal())]
|
||||
if !ok {
|
||||
wish.Println(ss, "huh?")
|
||||
_ = ss.Close()
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case ssh.KeysEqual(ss.PublicKey(), k):
|
||||
h(ss)
|
||||
default:
|
||||
wish.Println(ss, "FUBAR")
|
||||
_ = ss.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) ziggssh(h ssh.Handler) ssh.Handler {
|
||||
return func(ss ssh.Session) {
|
||||
cli.StartCLI()
|
||||
h(ss)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) ListenAndServe() error {
|
||||
var err error
|
||||
s.Server, err = wish.NewServer(
|
||||
wish.WithAddress(config.SSHListen),
|
||||
wish.WithHostKeyPath(config.SSHHostKey),
|
||||
wish.WithVersion("SSH-2.0-ziggs"),
|
||||
wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
return true
|
||||
}),
|
||||
wish.WithMiddleware(
|
||||
logging.Middleware(),
|
||||
s.checkAuth,
|
||||
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
done := make(chan os.Signal, 1)
|
||||
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
log.Printf("Starting SSH server on %s", config.SSHListen)
|
||||
go func() {
|
||||
if err = s.ListenAndServe(); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
<-done
|
||||
log.Println("Stopping SSH server")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer func() { cancel() }()
|
||||
if err := s.Shutdown(ctx); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
|
@ -14,6 +14,7 @@ type Config struct {
|
|||
ProxyPort uint16 `json:"proxyport,omitempty"`
|
||||
LinkButton bool `json:"linkbutton,omitempty"`
|
||||
IPAddress string `json:"ipaddress,omitempty"`
|
||||
Reboot bool `json:"reboot,omitempty"`
|
||||
Mac string `json:"mac,omitempty"`
|
||||
NetMask string `json:"netmask,omitempty"`
|
||||
Gateway string `json:"gateway,omitempty"`
|
||||
|
@ -30,7 +31,6 @@ type Config struct {
|
|||
DatastoreVersion string `json:"datastoreversion,omitempty"`
|
||||
StarterKitID string `json:"starterkitid,omitempty"`
|
||||
InternetService InternetService `json:"internetservices,omitempty"`
|
||||
Reboot bool `json:"reboot,omitempty"`
|
||||
}
|
||||
|
||||
// SwUpdate contains information related to software updates. Deprecated in 1.20
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Ayman Bagabas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,25 @@
|
|||
|
||||
# go-osc52
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/aymanbagabas/go-osc52/releases"><img src="https://img.shields.io/github/release/aymanbagabas/go-osc52.svg" alt="Latest Release"></a>
|
||||
<a href="https://pkg.go.dev/github.com/aymanbagabas/go-osc52?tab=doc"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="GoDoc"></a>
|
||||
</p>
|
||||
|
||||
A terminal Go library to copy text to clipboard from anywhere. It does so using [ANSI OSC52](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands). The `Copy()` function defaults to copying text from terminals running locally.
|
||||
|
||||
To use this over SSH, using [gliderlabs/ssh](https://github.com/gliderlabs/ssh), use `NewOutput(sshSession, sshSession.Environ())` and make sure you pass the `TERM` environment variable in your SSH connection.
|
||||
|
||||
```sh
|
||||
ssh -o SendEnv=TERM <host>
|
||||
```
|
||||
|
||||
Tmux users need to pass an additional environment variable `TMUX`.
|
||||
|
||||
```sh
|
||||
ssh -o SendEnv=TERM -o SendEnv=TMUX <host>
|
||||
```
|
||||
|
||||
# Credits
|
||||
|
||||
* [vim-oscyank](https://github.com/ojroques/vim-oscyank) this is heavily inspired by vim-oscyank.
|
|
@ -0,0 +1,110 @@
|
|||
package osc52
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// output is the default output for Copy which uses os.Stdout and os.Environ.
|
||||
var output = NewOutput(os.Stdout, os.Environ())
|
||||
|
||||
// envs is a map of environment variables.
|
||||
type envs map[string]string
|
||||
|
||||
// Get returns the value of the environment variable named by the key.
|
||||
func (e envs) Get(key string) string {
|
||||
v, ok := e[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Output is where the OSC52 string should be written.
|
||||
type Output struct {
|
||||
out io.Writer
|
||||
envs envs
|
||||
}
|
||||
|
||||
// NewOutput returns a new Output.
|
||||
func NewOutput(out io.Writer, envs []string) *Output {
|
||||
e := make(map[string]string, 0)
|
||||
for _, env := range envs {
|
||||
s := strings.Split(env, "=")
|
||||
k := s[0]
|
||||
v := strings.Join(s[1:], "=")
|
||||
e[k] = v
|
||||
}
|
||||
o := &Output{
|
||||
out: out,
|
||||
envs: e,
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
// Copy copies the OSC52 string to the output. This is the default copy function.
|
||||
func Copy(str string) {
|
||||
output.Copy(str)
|
||||
}
|
||||
|
||||
// Copy copies the OSC52 string to the output.
|
||||
func (o *Output) Copy(str string) {
|
||||
mode := "default"
|
||||
term := o.envs.Get("TERM")
|
||||
switch {
|
||||
case o.envs.Get("TMUX") != "", strings.HasPrefix(term, "tmux"):
|
||||
mode = "tmux"
|
||||
case strings.HasPrefix(term, "screen"):
|
||||
mode = "screen"
|
||||
case strings.Contains(term, "kitty"):
|
||||
mode = "kitty"
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case "default":
|
||||
o.copyDefault(str)
|
||||
case "tmux":
|
||||
o.copyTmux(str)
|
||||
case "screen":
|
||||
o.copyDCS(str)
|
||||
case "kitty":
|
||||
o.copyKitty(str)
|
||||
}
|
||||
}
|
||||
|
||||
// copyDefault copies the OSC52 string to the output.
|
||||
func (o *Output) copyDefault(str string) {
|
||||
b64 := base64.StdEncoding.EncodeToString([]byte(str))
|
||||
o.out.Write([]byte("\x1b]52;c;" + b64 + "\x07"))
|
||||
}
|
||||
|
||||
// copyTmux copies the OSC52 string escaped for Tmux.
|
||||
func (o *Output) copyTmux(str string) {
|
||||
b64 := base64.StdEncoding.EncodeToString([]byte(str))
|
||||
o.out.Write([]byte("\x1bPtmux;\x1b\x1b]52;c;" + b64 + "\x07\x1b\\"))
|
||||
}
|
||||
|
||||
// copyDCS copies the OSC52 string wrapped in a DCS sequence which is
|
||||
// appropriate when using screen.
|
||||
//
|
||||
// Screen doesn't support OSC52 but will pass the contents of a DCS sequence to
|
||||
// the outer terminal unchanged.
|
||||
func (o *Output) copyDCS(str string) {
|
||||
// Here, we split the encoded string into 76 bytes chunks and then join the
|
||||
// chunks with <end-dsc><start-dsc> sequences. Finally, wrap the whole thing in
|
||||
// <start-dsc><start-osc52><joined-chunks><end-osc52><end-dsc>.
|
||||
b64 := base64.StdEncoding.EncodeToString([]byte(str))
|
||||
s := strings.SplitN(b64, "", 76)
|
||||
q := fmt.Sprintf("\x1bP\x1b]52;c;%s\x07\x1b\x5c", strings.Join(s, "\x1b\\\x1bP"))
|
||||
o.out.Write([]byte(q))
|
||||
}
|
||||
|
||||
// copyKitty copies the OSC52 string to Kitty. First, it flushes the keyboard
|
||||
// before copying, this is required for Kitty < 0.22.0.
|
||||
func (o *Output) copyKitty(str string) {
|
||||
o.out.Write([]byte("\x1b]52;c;!\x07"))
|
||||
o.copyDefault(str)
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
dist/
|
|
@ -0,0 +1,3 @@
|
|||
includes:
|
||||
- from_url:
|
||||
url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/lib.yml
|
|
@ -0,0 +1,7 @@
|
|||
# sshmarshal
|
||||
|
||||
Library containing code copied from [x/crypto][crypto] and [this patch][patch] so we can
|
||||
more easily marshal SSH private keys.
|
||||
|
||||
[patch]: https://go-review.googlesource.com/c/crypto/+/218620/
|
||||
[crypto]: https://github.com/golang/crypto
|
93
vendor/github.com/caarlos0/sshmarshal/internal/bcrypt_pbkdf/bcrypt_pbkdf.go
generated
vendored
Normal file
93
vendor/github.com/caarlos0/sshmarshal/internal/bcrypt_pbkdf/bcrypt_pbkdf.go
generated
vendored
Normal file
|
@ -0,0 +1,93 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package bcrypt_pbkdf implements bcrypt_pbkdf(3) from OpenBSD.
|
||||
//
|
||||
// See https://flak.tedunangst.com/post/bcrypt-pbkdf and
|
||||
// https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/lib/libutil/bcrypt_pbkdf.c.
|
||||
package bcrypt_pbkdf
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"golang.org/x/crypto/blowfish"
|
||||
)
|
||||
|
||||
const blockSize = 32
|
||||
|
||||
// Key derives a key from the password, salt and rounds count, returning a
|
||||
// []byte of length keyLen that can be used as cryptographic key.
|
||||
func Key(password, salt []byte, rounds, keyLen int) ([]byte, error) {
|
||||
if rounds < 1 {
|
||||
return nil, errors.New("bcrypt_pbkdf: number of rounds is too small")
|
||||
}
|
||||
if len(password) == 0 {
|
||||
return nil, errors.New("bcrypt_pbkdf: empty password")
|
||||
}
|
||||
if len(salt) == 0 || len(salt) > 1<<20 {
|
||||
return nil, errors.New("bcrypt_pbkdf: bad salt length")
|
||||
}
|
||||
if keyLen > 1024 {
|
||||
return nil, errors.New("bcrypt_pbkdf: keyLen is too large")
|
||||
}
|
||||
|
||||
numBlocks := (keyLen + blockSize - 1) / blockSize
|
||||
key := make([]byte, numBlocks*blockSize)
|
||||
|
||||
h := sha512.New()
|
||||
h.Write(password)
|
||||
shapass := h.Sum(nil)
|
||||
|
||||
shasalt := make([]byte, 0, sha512.Size)
|
||||
cnt, tmp := make([]byte, 4), make([]byte, blockSize)
|
||||
for block := 1; block <= numBlocks; block++ {
|
||||
h.Reset()
|
||||
h.Write(salt)
|
||||
cnt[0] = byte(block >> 24)
|
||||
cnt[1] = byte(block >> 16)
|
||||
cnt[2] = byte(block >> 8)
|
||||
cnt[3] = byte(block)
|
||||
h.Write(cnt)
|
||||
bcryptHash(tmp, shapass, h.Sum(shasalt))
|
||||
|
||||
out := make([]byte, blockSize)
|
||||
copy(out, tmp)
|
||||
for i := 2; i <= rounds; i++ {
|
||||
h.Reset()
|
||||
h.Write(tmp)
|
||||
bcryptHash(tmp, shapass, h.Sum(shasalt))
|
||||
for j := 0; j < len(out); j++ {
|
||||
out[j] ^= tmp[j]
|
||||
}
|
||||
}
|
||||
|
||||
for i, v := range out {
|
||||
key[i*numBlocks+(block-1)] = v
|
||||
}
|
||||
}
|
||||
return key[:keyLen], nil
|
||||
}
|
||||
|
||||
var magic = []byte("OxychromaticBlowfishSwatDynamite")
|
||||
|
||||
func bcryptHash(out, shapass, shasalt []byte) {
|
||||
c, err := blowfish.NewSaltedCipher(shapass, shasalt)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for i := 0; i < 64; i++ {
|
||||
blowfish.ExpandKey(shasalt, c)
|
||||
blowfish.ExpandKey(shapass, c)
|
||||
}
|
||||
copy(out, magic)
|
||||
for i := 0; i < 32; i += 8 {
|
||||
for j := 0; j < 64; j++ {
|
||||
c.Encrypt(out[i:i+8], out[i:i+8])
|
||||
}
|
||||
}
|
||||
// Swap bytes due to different endianness.
|
||||
for i := 0; i < 32; i += 4 {
|
||||
out[i+3], out[i+2], out[i+1], out[i] = out[i], out[i+1], out[i+2], out[i+3]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,236 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sshmarshal
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
|
||||
"github.com/caarlos0/sshmarshal/internal/bcrypt_pbkdf"
|
||||
. "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// MarshalPrivateKey returns a PEM block with the private key serialized in the
|
||||
// OpenSSH format.
|
||||
func MarshalPrivateKey(key crypto.PrivateKey, comment string) (*pem.Block, error) {
|
||||
return marshalOpenSSHPrivateKey(key, comment, unencryptedOpenSSHMarshaler)
|
||||
}
|
||||
|
||||
// MarshalPrivateKeyWithPassphrase returns a PEM block holding the encrypted
|
||||
// private key serialized in the OpenSSH format.
|
||||
func MarshalPrivateKeyWithPassphrase(key crypto.PrivateKey, comment string, passphrase []byte) (*pem.Block, error) {
|
||||
return marshalOpenSSHPrivateKey(key, comment, passphraseProtectedOpenSSHMarshaler(passphrase))
|
||||
}
|
||||
|
||||
func unencryptedOpenSSHMarshaler(privKeyBlock []byte) ([]byte, string, string, string, error) {
|
||||
key := generateOpenSSHPadding(privKeyBlock, 8)
|
||||
return key, "none", "none", "", nil
|
||||
}
|
||||
|
||||
func passphraseProtectedOpenSSHMarshaler(passphrase []byte) openSSHEncryptFunc {
|
||||
return func(privKeyBlock []byte) ([]byte, string, string, string, error) {
|
||||
salt := make([]byte, 16)
|
||||
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
|
||||
opts := struct {
|
||||
Salt []byte
|
||||
Rounds uint32
|
||||
}{salt, 16}
|
||||
|
||||
// Derive key to encrypt the private key block.
|
||||
k, err := bcrypt_pbkdf.Key(passphrase, salt, int(opts.Rounds), 32+aes.BlockSize)
|
||||
if err != nil {
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
|
||||
// Add padding matching the block size of AES.
|
||||
keyBlock := generateOpenSSHPadding(privKeyBlock, aes.BlockSize)
|
||||
|
||||
// Encrypt the private key using the derived secret.
|
||||
dst := make([]byte, len(keyBlock))
|
||||
iv := k[32 : 32+aes.BlockSize]
|
||||
block, err := aes.NewCipher(k[:32])
|
||||
if err != nil {
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
|
||||
stream := cipher.NewCTR(block, iv)
|
||||
stream.XORKeyStream(dst, keyBlock)
|
||||
|
||||
return dst, "aes256-ctr", "bcrypt", string(Marshal(opts)), nil
|
||||
}
|
||||
}
|
||||
|
||||
const magic = "openssh-key-v1\x00"
|
||||
|
||||
type openSSHEncryptFunc func(privKeyBlock []byte) (protectedKeyBlock []byte, cipherName, kdfName, kdfOptions string, err error)
|
||||
|
||||
func marshalOpenSSHPrivateKey(key crypto.PrivateKey, comment string, encrypt openSSHEncryptFunc) (*pem.Block, error) {
|
||||
var w struct {
|
||||
CipherName string
|
||||
KdfName string
|
||||
KdfOpts string
|
||||
NumKeys uint32
|
||||
PubKey []byte
|
||||
PrivKeyBlock []byte
|
||||
}
|
||||
var pk1 struct {
|
||||
Check1 uint32
|
||||
Check2 uint32
|
||||
Keytype string
|
||||
Rest []byte `ssh:"rest"`
|
||||
}
|
||||
|
||||
// Random check bytes.
|
||||
var check uint32
|
||||
if err := binary.Read(rand.Reader, binary.BigEndian, &check); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pk1.Check1 = check
|
||||
pk1.Check2 = check
|
||||
w.NumKeys = 1
|
||||
|
||||
// Use a []byte directly on ed25519 keys.
|
||||
if k, ok := key.(*ed25519.PrivateKey); ok {
|
||||
key = *k
|
||||
}
|
||||
|
||||
switch k := key.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
E := new(big.Int).SetInt64(int64(k.PublicKey.E))
|
||||
// Marshal public key:
|
||||
// E and N are in reversed order in the public and private key.
|
||||
pubKey := struct {
|
||||
KeyType string
|
||||
E *big.Int
|
||||
N *big.Int
|
||||
}{
|
||||
KeyAlgoRSA,
|
||||
E, k.PublicKey.N,
|
||||
}
|
||||
w.PubKey = Marshal(pubKey)
|
||||
|
||||
// Marshal private key.
|
||||
key := struct {
|
||||
N *big.Int
|
||||
E *big.Int
|
||||
D *big.Int
|
||||
Iqmp *big.Int
|
||||
P *big.Int
|
||||
Q *big.Int
|
||||
Comment string
|
||||
}{
|
||||
k.PublicKey.N, E,
|
||||
k.D, k.Precomputed.Qinv, k.Primes[0], k.Primes[1],
|
||||
comment,
|
||||
}
|
||||
pk1.Keytype = KeyAlgoRSA
|
||||
pk1.Rest = Marshal(key)
|
||||
case ed25519.PrivateKey:
|
||||
pub := make([]byte, ed25519.PublicKeySize)
|
||||
priv := make([]byte, ed25519.PrivateKeySize)
|
||||
copy(pub, k[ed25519.PublicKeySize:])
|
||||
copy(priv, k)
|
||||
|
||||
// Marshal public key.
|
||||
pubKey := struct {
|
||||
KeyType string
|
||||
Pub []byte
|
||||
}{
|
||||
KeyAlgoED25519, pub,
|
||||
}
|
||||
w.PubKey = Marshal(pubKey)
|
||||
|
||||
// Marshal private key.
|
||||
key := struct {
|
||||
Pub []byte
|
||||
Priv []byte
|
||||
Comment string
|
||||
}{
|
||||
pub, priv,
|
||||
comment,
|
||||
}
|
||||
pk1.Keytype = KeyAlgoED25519
|
||||
pk1.Rest = Marshal(key)
|
||||
case *ecdsa.PrivateKey:
|
||||
var curve, keyType string
|
||||
switch name := k.Curve.Params().Name; name {
|
||||
case "P-256":
|
||||
curve = "nistp256"
|
||||
keyType = KeyAlgoECDSA256
|
||||
case "P-384":
|
||||
curve = "nistp384"
|
||||
keyType = KeyAlgoECDSA384
|
||||
case "P-521":
|
||||
curve = "nistp521"
|
||||
keyType = KeyAlgoECDSA521
|
||||
default:
|
||||
return nil, errors.New("ssh: unhandled elliptic curve " + name)
|
||||
}
|
||||
|
||||
pub := elliptic.Marshal(k.Curve, k.PublicKey.X, k.PublicKey.Y)
|
||||
|
||||
// Marshal public key.
|
||||
pubKey := struct {
|
||||
KeyType string
|
||||
Curve string
|
||||
Pub []byte
|
||||
}{
|
||||
keyType, curve, pub,
|
||||
}
|
||||
w.PubKey = Marshal(pubKey)
|
||||
|
||||
// Marshal private key.
|
||||
key := struct {
|
||||
Curve string
|
||||
Pub []byte
|
||||
D *big.Int
|
||||
Comment string
|
||||
}{
|
||||
curve, pub, k.D,
|
||||
comment,
|
||||
}
|
||||
pk1.Keytype = keyType
|
||||
pk1.Rest = Marshal(key)
|
||||
default:
|
||||
return nil, fmt.Errorf("ssh: unsupported key type %T", k)
|
||||
}
|
||||
|
||||
var err error
|
||||
// Add padding and encrypt the key if necessary.
|
||||
w.PrivKeyBlock, w.CipherName, w.KdfName, w.KdfOpts, err = encrypt(Marshal(pk1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b := Marshal(w)
|
||||
block := &pem.Block{
|
||||
Type: "OPENSSH PRIVATE KEY",
|
||||
Bytes: append([]byte(magic), b...),
|
||||
}
|
||||
return block, nil
|
||||
}
|
||||
|
||||
func generateOpenSSHPadding(block []byte, blockSize int) []byte {
|
||||
for i, l := 0, len(block); (l+i)%blockSize != 0; i++ {
|
||||
block = append(block, byte(i+1))
|
||||
}
|
||||
return block
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
run:
|
||||
tests: false
|
||||
|
||||
issues:
|
||||
include:
|
||||
- EXC0001
|
||||
- EXC0005
|
||||
- EXC0011
|
||||
- EXC0012
|
||||
- EXC0013
|
||||
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- bodyclose
|
||||
- dupl
|
||||
- exportloopref
|
||||
- goconst
|
||||
- godot
|
||||
- godox
|
||||
- goimports
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- ifshort
|
||||
- misspell
|
||||
- prealloc
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- whitespace
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Charmbracelet, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,32 @@
|
|||
# Keygen
|
||||
|
||||
[![Latest Release](https://img.shields.io/github/release/charmbracelet/keygen.svg)](https://github.com/charmbracelet/keygen/releases)
|
||||
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/keygen?tab=doc)
|
||||
[![Build Status](https://github.com/charmbracelet/keygen/workflows/build/badge.svg)](https://github.com/charmbracelet/keygen/actions)
|
||||
[![Go ReportCard](https://goreportcard.com/badge/charmbracelet/keygen)](https://goreportcard.com/report/charmbracelet/keygen)
|
||||
|
||||
An SSH key pair generator with password protected keys support. Supports generating RSA, ECDSA, and Ed25519 keys.
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
filepath := filepath.Join(".ssh", "my_awesome_key")
|
||||
passphrase := []byte("awesome_secret")
|
||||
k, err := NewWithWrite(filepath, passphrase, key.Ed25519)
|
||||
if err != nil {
|
||||
fmt.Printf("error creating SSH key pair: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/charmbracelet/keygen/raw/master/LICENSE)
|
||||
|
||||
***
|
||||
|
||||
Part of [Charm](https://charm.sh).
|
||||
|
||||
<a href="https://charm.sh/"><img alt="the Charm logo" src="https://stuff.charm.sh/charm-badge-unrounded.jpg" width="400"></a>
|
||||
|
||||
Charm热爱开源 • Charm loves open source
|
|
@ -0,0 +1,387 @@
|
|||
// Package keygen handles the creation of new SSH key pairs.
|
||||
package keygen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/caarlos0/sshmarshal"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// KeyType represents a type of SSH key.
|
||||
type KeyType string
|
||||
|
||||
// Supported key types.
|
||||
const (
|
||||
RSA KeyType = "rsa"
|
||||
Ed25519 KeyType = "ed25519"
|
||||
ECDSA KeyType = "ecdsa"
|
||||
)
|
||||
|
||||
const rsaDefaultBits = 4096
|
||||
|
||||
// ErrMissingSSHKeys indicates we're missing some keys that we expected to
|
||||
// have after generating. This should be an extreme edge case.
|
||||
var ErrMissingSSHKeys = errors.New("missing one or more keys; did something happen to them after they were generated?")
|
||||
|
||||
// ErrUnsupportedKeyType indicates an unsupported key type.
|
||||
type ErrUnsupportedKeyType struct {
|
||||
keyType string
|
||||
}
|
||||
|
||||
// Error implements the error interface for ErrUnsupportedKeyType
|
||||
func (e ErrUnsupportedKeyType) Error() string {
|
||||
err := "unsupported key type"
|
||||
if e.keyType != "" {
|
||||
err += fmt.Sprintf(": %s", e.keyType)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// FilesystemErr is used to signal there was a problem creating keys at the
|
||||
// filesystem-level. For example, when we're unable to create a directory to
|
||||
// store new SSH keys in.
|
||||
type FilesystemErr struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error returns a human-readable string for the error. It implements the error
|
||||
// interface.
|
||||
func (e FilesystemErr) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying error.
|
||||
func (e FilesystemErr) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// SSHKeysAlreadyExistErr indicates that files already exist at the location at
|
||||
// which we're attempting to create SSH keys.
|
||||
type SSHKeysAlreadyExistErr struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// SSHKeyPair holds a pair of SSH keys and associated methods.
|
||||
type SSHKeyPair struct {
|
||||
path string // private key filename path; public key will have .pub appended
|
||||
passphrase []byte
|
||||
keyType KeyType
|
||||
privateKey crypto.PrivateKey
|
||||
}
|
||||
|
||||
func (s SSHKeyPair) privateKeyPath() string {
|
||||
p := fmt.Sprintf("%s_%s", s.path, s.keyType)
|
||||
return p
|
||||
}
|
||||
|
||||
func (s SSHKeyPair) publicKeyPath() string {
|
||||
return s.privateKeyPath() + ".pub"
|
||||
}
|
||||
|
||||
// New generates an SSHKeyPair, which contains a pair of SSH keys.
|
||||
func New(path string, passphrase []byte, keyType KeyType) (*SSHKeyPair, error) {
|
||||
var err error
|
||||
s := &SSHKeyPair{
|
||||
path: path,
|
||||
keyType: keyType,
|
||||
passphrase: passphrase,
|
||||
}
|
||||
if s.KeyPairExists() {
|
||||
privData, err := ioutil.ReadFile(s.privateKeyPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var k interface{}
|
||||
if len(passphrase) > 0 {
|
||||
k, err = ssh.ParseRawPrivateKeyWithPassphrase(privData, passphrase)
|
||||
} else {
|
||||
k, err = ssh.ParseRawPrivateKey(privData)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch k := k.(type) {
|
||||
case *rsa.PrivateKey, *ecdsa.PrivateKey, *ed25519.PrivateKey:
|
||||
s.privateKey = k
|
||||
default:
|
||||
return nil, ErrUnsupportedKeyType{fmt.Sprintf("%T", k)}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
switch keyType {
|
||||
case Ed25519:
|
||||
err = s.generateEd25519Keys()
|
||||
case RSA:
|
||||
err = s.generateRSAKeys(rsaDefaultBits)
|
||||
case ECDSA:
|
||||
err = s.generateECDSAKeys(elliptic.P384())
|
||||
default:
|
||||
return nil, ErrUnsupportedKeyType{string(keyType)}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// NewWithWrite generates an SSHKeyPair and writes it to disk if not exist.
|
||||
func NewWithWrite(path string, passphrase []byte, keyType KeyType) (*SSHKeyPair, error) {
|
||||
s, err := New(path, passphrase, keyType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !s.KeyPairExists() {
|
||||
if err = s.WriteKeys(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// PrivateKey returns the unencrypted private key.
|
||||
func (s *SSHKeyPair) PrivateKey() crypto.PrivateKey {
|
||||
switch s.keyType {
|
||||
case RSA, Ed25519, ECDSA:
|
||||
return s.privateKey
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PrivateKeyPEM returns the unencrypted private key in OPENSSH PEM format.
|
||||
func (s *SSHKeyPair) PrivateKeyPEM() []byte {
|
||||
block, err := s.pemBlock(nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return pem.EncodeToMemory(block)
|
||||
}
|
||||
|
||||
// PublicKey returns the SSH public key (RFC 4253). Ready to be used in an
|
||||
// OpenSSH authorized_keys file.
|
||||
func (s *SSHKeyPair) PublicKey() []byte {
|
||||
var pk crypto.PublicKey
|
||||
// Prepare public key
|
||||
switch s.keyType {
|
||||
case RSA:
|
||||
key, ok := s.privateKey.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
pk = key.Public()
|
||||
case Ed25519:
|
||||
key, ok := s.privateKey.(*ed25519.PrivateKey)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
pk = key.Public()
|
||||
case ECDSA:
|
||||
key, ok := s.privateKey.(*ecdsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
pk = key.Public()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
p, err := ssh.NewPublicKey(pk)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
// serialize public key
|
||||
ak := ssh.MarshalAuthorizedKey(p)
|
||||
return pubKeyWithMemo(ak)
|
||||
}
|
||||
|
||||
func (s *SSHKeyPair) pemBlock(passphrase []byte) (*pem.Block, error) {
|
||||
key := s.PrivateKey()
|
||||
if key == nil {
|
||||
return nil, ErrMissingSSHKeys
|
||||
}
|
||||
switch s.keyType {
|
||||
case RSA, Ed25519, ECDSA:
|
||||
if len(passphrase) > 0 {
|
||||
return sshmarshal.MarshalPrivateKeyWithPassphrase(key, "", passphrase)
|
||||
}
|
||||
return sshmarshal.MarshalPrivateKey(key, "")
|
||||
default:
|
||||
return nil, ErrUnsupportedKeyType{string(s.keyType)}
|
||||
}
|
||||
}
|
||||
|
||||
// generateEd25519Keys creates a pair of EdD25519 keys for SSH auth.
|
||||
func (s *SSHKeyPair) generateEd25519Keys() error {
|
||||
// Generate keys
|
||||
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.privateKey = &privateKey
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateEd25519Keys creates a pair of EdD25519 keys for SSH auth.
|
||||
func (s *SSHKeyPair) generateECDSAKeys(curve elliptic.Curve) error {
|
||||
// Generate keys
|
||||
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.privateKey = privateKey
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateRSAKeys creates a pair for RSA keys for SSH auth.
|
||||
func (s *SSHKeyPair) generateRSAKeys(bitSize int) error {
|
||||
// Generate private key
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, bitSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Validate private key
|
||||
err = privateKey.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.privateKey = privateKey
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepFilesystem makes sure the state of the filesystem is as it needs to be
|
||||
// in order to write our keys to disk. It will create and/or set permissions on
|
||||
// the SSH directory we're going to write our keys to (for example, ~/.ssh) as
|
||||
// well as make sure that no files exist at the location in which we're going
|
||||
// to write out keys.
|
||||
func (s *SSHKeyPair) prepFilesystem() error {
|
||||
var err error
|
||||
|
||||
keyDir := filepath.Dir(s.path)
|
||||
if keyDir != "" {
|
||||
keyDir, err = homedir.Expand(keyDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info, err := os.Stat(keyDir)
|
||||
if os.IsNotExist(err) {
|
||||
// Directory doesn't exist: create it
|
||||
return os.MkdirAll(keyDir, 0700)
|
||||
}
|
||||
if err != nil {
|
||||
// There was another error statting the directory; something is awry
|
||||
return FilesystemErr{Err: err}
|
||||
}
|
||||
if !info.IsDir() {
|
||||
// It exists but it's not a directory
|
||||
return FilesystemErr{Err: fmt.Errorf("%s is not a directory", keyDir)}
|
||||
}
|
||||
if info.Mode().Perm() != 0700 {
|
||||
// Permissions are wrong: fix 'em
|
||||
if err := os.Chmod(keyDir, 0700); err != nil {
|
||||
return FilesystemErr{Err: err}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the files we're going to write to don't already exist
|
||||
if fileExists(s.privateKeyPath()) {
|
||||
return SSHKeysAlreadyExistErr{Path: s.privateKeyPath()}
|
||||
}
|
||||
if fileExists(s.publicKeyPath()) {
|
||||
return SSHKeysAlreadyExistErr{Path: s.publicKeyPath()}
|
||||
}
|
||||
|
||||
// The directory looks good as-is
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteKeys writes the SSH key pair to disk.
|
||||
func (s *SSHKeyPair) WriteKeys() error {
|
||||
var err error
|
||||
priv := s.PrivateKeyPEM()
|
||||
pub := s.PublicKey()
|
||||
if priv == nil || pub == nil {
|
||||
return ErrMissingSSHKeys
|
||||
}
|
||||
|
||||
// Encrypt private key with passphrase
|
||||
if len(s.passphrase) > 0 {
|
||||
block, err := s.pemBlock(s.passphrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
priv = pem.EncodeToMemory(block)
|
||||
}
|
||||
if err = s.prepFilesystem(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := writeKeyToFile(priv, s.privateKeyPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeKeyToFile(pub, s.publicKeyPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyPairExists checks if the SSH key pair exists on disk.
|
||||
func (s *SSHKeyPair) KeyPairExists() bool {
|
||||
return fileExists(s.privateKeyPath()) && fileExists(s.publicKeyPath())
|
||||
}
|
||||
|
||||
func writeKeyToFile(keyBytes []byte, path string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return ioutil.WriteFile(path, keyBytes, 0600)
|
||||
}
|
||||
return FilesystemErr{Err: fmt.Errorf("file %s already exists", path)}
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// attaches a user@host suffix to a serialized public key. returns the original
|
||||
// pubkey if we can't get the username or host.
|
||||
func pubKeyWithMemo(pubKey []byte) []byte {
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return pubKey
|
||||
}
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return pubKey
|
||||
}
|
||||
|
||||
return append(bytes.TrimRight(pubKey, "\n"), []byte(fmt.Sprintf(" %s@%s\n", u.Username, hostname))...)
|
||||
}
|
||||
|
||||
// Error returns the a human-readable error message for SSHKeysAlreadyExistErr.
|
||||
// It satisfies the error interface.
|
||||
func (e SSHKeysAlreadyExistErr) Error() string {
|
||||
return fmt.Sprintf("ssh key %s already exists", e.Path)
|
||||
}
|
|
@ -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.
|
|
@ -0,0 +1,96 @@
|
|||
# gliderlabs/ssh
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/gliderlabs/ssh?status.svg)](https://godoc.org/github.com/gliderlabs/ssh)
|
||||
[![CircleCI](https://img.shields.io/circleci/project/github/gliderlabs/ssh.svg)](https://circleci.com/gh/gliderlabs/ssh)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/gliderlabs/ssh)](https://goreportcard.com/report/github.com/gliderlabs/ssh)
|
||||
[![OpenCollective](https://opencollective.com/ssh/sponsors/badge.svg)](#sponsors)
|
||||
[![Slack](http://slack.gliderlabs.com/badge.svg)](http://slack.gliderlabs.com)
|
||||
[![Email Updates](https://img.shields.io/badge/updates-subscribe-yellow.svg)](https://app.convertkit.com/landing_pages/243312)
|
||||
|
||||
> The Glider Labs SSH server package is dope. —[@bradfitz](https://twitter.com/bradfitz), Go team member
|
||||
|
||||
This Go package wraps the [crypto/ssh
|
||||
package](https://godoc.org/golang.org/x/crypto/ssh) with a higher-level API for
|
||||
building SSH servers. The goal of the API was to make it as simple as using
|
||||
[net/http](https://golang.org/pkg/net/http/), so the API is very similar:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gliderlabs/ssh"
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ssh.Handle(func(s ssh.Session) {
|
||||
io.WriteString(s, "Hello world\n")
|
||||
})
|
||||
|
||||
log.Fatal(ssh.ListenAndServe(":2222", nil))
|
||||
}
|
||||
|
||||
```
|
||||
This package was built by [@progrium](https://twitter.com/progrium) after working on nearly a dozen projects at Glider Labs using SSH and collaborating with [@shazow](https://twitter.com/shazow) (known for [ssh-chat](https://github.com/shazow/ssh-chat)).
|
||||
|
||||
## Examples
|
||||
|
||||
A bunch of great examples are in the `_examples` directory.
|
||||
|
||||
## Usage
|
||||
|
||||
[See GoDoc reference.](https://godoc.org/github.com/gliderlabs/ssh)
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are welcome! However, since this project is very much about API
|
||||
design, please submit API changes as issues to discuss before submitting PRs.
|
||||
|
||||
Also, you can [join our Slack](http://slack.gliderlabs.com) to discuss as well.
|
||||
|
||||
## Roadmap
|
||||
|
||||
* Non-session channel handlers
|
||||
* Cleanup callback API
|
||||
* 1.0 release
|
||||
* High-level client?
|
||||
|
||||
## Sponsors
|
||||
|
||||
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/ssh#sponsor)]
|
||||
|
||||
<a href="https://opencollective.com/ssh/sponsor/0/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/1/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/2/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/3/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/4/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/5/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/6/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/7/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/8/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/9/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/9/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/10/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/10/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/11/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/11/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/12/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/12/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/13/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/13/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/14/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/14/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/15/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/15/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/16/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/16/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/17/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/17/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/18/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/18/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/19/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/19/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/20/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/20/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/21/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/21/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/22/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/22/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/23/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/23/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/24/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/24/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/25/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/25/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/26/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/26/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/27/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/27/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/28/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/28/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/ssh/sponsor/29/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/29/avatar.svg"></a>
|
||||
|
||||
## License
|
||||
|
||||
[BSD](LICENSE)
|
|
@ -0,0 +1,83 @@
|
|||
package ssh
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"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 := ioutil.TempDir("", 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 conn.Close()
|
||||
channel, reqs, err := sshConn.OpenChannel(agentChannelType, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer channel.Close()
|
||||
go gossh.DiscardRequests(reqs)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
io.Copy(conn, channel)
|
||||
conn.(*net.UnixConn).CloseWrite()
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
io.Copy(channel, conn)
|
||||
channel.CloseWrite()
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
}(conn)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
version: 2
|
||||
jobs:
|
||||
build-go-latest:
|
||||
docker:
|
||||
- image: golang:latest
|
||||
working_directory: /go/src/github.com/gliderlabs/ssh
|
||||
steps:
|
||||
- checkout
|
||||
- run: go get
|
||||
- run: go test -v -race
|
||||
|
||||
build-go-1.13:
|
||||
docker:
|
||||
- image: golang:1.13
|
||||
working_directory: /go/src/github.com/gliderlabs/ssh
|
||||
steps:
|
||||
- checkout
|
||||
- run: go get
|
||||
- run: go test -v -race
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- build-go-latest
|
||||
- build-go-1.13
|
|
@ -0,0 +1,55 @@
|
|||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type serverConn struct {
|
||||
net.Conn
|
||||
|
||||
idleTimeout time.Duration
|
||||
maxDeadline time.Time
|
||||
closeCanceler context.CancelFunc
|
||||
}
|
||||
|
||||
func (c *serverConn) Write(p []byte) (n int, err error) {
|
||||
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) {
|
||||
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() {
|
||||
switch {
|
||||
case c.idleTimeout > 0:
|
||||
idleDeadline := time.Now().Add(c.idleTimeout)
|
||||
if idleDeadline.Unix() < c.maxDeadline.Unix() || c.maxDeadline.IsZero() {
|
||||
c.Conn.SetDeadline(idleDeadline)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
c.Conn.SetDeadline(c.maxDeadline)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
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"}
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func newContext(srv *Server) (*sshContext, context.CancelFunc) {
|
||||
innerCtx, cancel := context.WithCancel(context.Background())
|
||||
ctx := &sshContext{innerCtx, &sync.Mutex{}}
|
||||
ctx.SetValue(ContextKeyServer, srv)
|
||||
perms := &Permissions{&gossh.Permissions{}}
|
||||
ctx.SetValue(ContextKeyPermissions, perms)
|
||||
return ctx, cancel
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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(ContextKeyUser, conn.User())
|
||||
ctx.SetValue(ContextKeyLocalAddr, conn.LocalAddr())
|
||||
ctx.SetValue(ContextKeyRemoteAddr, conn.RemoteAddr())
|
||||
}
|
||||
|
||||
func (ctx *sshContext) SetValue(key, value interface{}) {
|
||||
ctx.Context = context.WithValue(ctx.Context, 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 {
|
||||
return ctx.Value(ContextKeyLocalAddr).(net.Addr)
|
||||
}
|
||||
|
||||
func (ctx *sshContext) Permissions() *Permissions {
|
||||
return ctx.Value(ContextKeyPermissions).(*Permissions)
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,84 @@
|
|||
package ssh
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
||||
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 := ioutil.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signer, err := gossh.ParsePrivateKey(pemBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srv.AddHostKey(signer)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
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(ctx Context, pty 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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
// 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.Replace(p, []byte{'\n'}, []byte{'\r', '\n'}, -1)
|
||||
p = bytes.Replace(p, []byte{'\r', '\r', '\n'}, []byte{'\r', '\n'}, -1)
|
||||
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)
|
||||
}
|
|
@ -0,0 +1,449 @@
|
|||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
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")
|
||||
|
||||
type SubsystemHandler func(s Session)
|
||||
|
||||
var DefaultSubsystemHandlers = map[string]SubsystemHandler{}
|
||||
|
||||
type RequestHandler func(ctx Context, srv *Server, req *gossh.Request) (ok bool, payload []byte)
|
||||
|
||||
var DefaultRequestHandlers = map[string]RequestHandler{}
|
||||
|
||||
type ChannelHandler func(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context)
|
||||
|
||||
var DefaultChannelHandlers = map[string]ChannelHandler{
|
||||
"session": DefaultSessionHandler,
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler
|
||||
PasswordHandler PasswordHandler // password authentication handler
|
||||
PublicKeyHandler PublicKeyHandler // public key authentication handler
|
||||
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all 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
|
||||
|
||||
IdleTimeout time.Duration // connection timeout when no activity, none if empty
|
||||
MaxTimeout time.Duration // absolute connection timeout, none if empty
|
||||
|
||||
// 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
|
||||
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.RLock()
|
||||
defer srv.mu.RUnlock()
|
||||
|
||||
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.NoClientAuth = true
|
||||
}
|
||||
if srv.Version != "" {
|
||||
config.ServerVersion = "SSH-2.0-" + srv.Version
|
||||
}
|
||||
if srv.PasswordHandler != nil {
|
||||
config.PasswordCallback = func(conn gossh.ConnMetadata, password []byte) (*gossh.Permissions, error) {
|
||||
applyConnMetadata(ctx, conn)
|
||||
if ok := srv.PasswordHandler(ctx, string(password)); !ok {
|
||||
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
|
||||
}
|
||||
return ctx.Permissions().Permissions, nil
|
||||
}
|
||||
}
|
||||
if srv.PublicKeyHandler != nil {
|
||||
config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) {
|
||||
applyConnMetadata(ctx, conn)
|
||||
if ok := srv.PublicKeyHandler(ctx, key); !ok {
|
||||
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
|
||||
}
|
||||
ctx.SetValue(ContextKeyPublicKey, key)
|
||||
return ctx.Permissions().Permissions, nil
|
||||
}
|
||||
}
|
||||
if srv.KeyboardInteractiveHandler != nil {
|
||||
config.KeyboardInteractiveCallback = func(conn gossh.ConnMetadata, challenger gossh.KeyboardInteractiveChallenge) (*gossh.Permissions, error) {
|
||||
applyConnMetadata(ctx, conn)
|
||||
if ok := srv.KeyboardInteractiveHandler(ctx, challenger); !ok {
|
||||
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
|
||||
}
|
||||
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 {
|
||||
srv.ensureHandlers()
|
||||
defer 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)
|
||||
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() {
|
||||
if tempDelay == 0 {
|
||||
tempDelay = 5 * time.Millisecond
|
||||
} else {
|
||||
tempDelay *= 2
|
||||
}
|
||||
if max := 1 * time.Second; tempDelay > max {
|
||||
tempDelay = max
|
||||
}
|
||||
time.Sleep(tempDelay)
|
||||
continue
|
||||
}
|
||||
return e
|
||||
}
|
||||
go srv.HandleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
defer conn.Close()
|
||||
sshConn, chans, reqs, err := gossh.NewServerConn(conn, srv.config(ctx))
|
||||
if err != nil {
|
||||
if srv.ConnectionFailedCallback != nil {
|
||||
srv.ConnectionFailedCallback(conn, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
srv.trackConn(sshConn, true)
|
||||
defer srv.trackConn(sshConn, false)
|
||||
|
||||
ctx.SetValue(ContextKeyConn, sshConn)
|
||||
applyConnMetadata(ctx, sshConn)
|
||||
//go gossh.DiscardRequests(reqs)
|
||||
go 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 handler(srv, sshConn, ch, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
func (srv *Server) SetOption(option Option) error {
|
||||
// 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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,371 @@
|
|||
package ssh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"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
|
||||
|
||||
// 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
|
||||
|
||||
func DefaultSessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
|
||||
ch, reqs, err := newChan.Accept()
|
||||
if err != nil {
|
||||
// TODO: trigger event callback
|
||||
return
|
||||
}
|
||||
sess := &session{
|
||||
Channel: ch,
|
||||
conn: conn,
|
||||
handler: srv.Handler,
|
||||
ptyCb: srv.PtyCallback,
|
||||
sessReqCb: srv.SessionRequestCallback,
|
||||
subsystemHandlers: srv.SubsystemHandlers,
|
||||
ctx: ctx,
|
||||
}
|
||||
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
|
||||
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 {
|
||||
return NewPtyReadWriter(sess.Channel.Stderr())
|
||||
}
|
||||
return sess.Channel.Stderr()
|
||||
}
|
||||
|
||||
func (sess *session) Write(p []byte) (int, error) {
|
||||
if sess.pty != nil {
|
||||
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) Pty() (Pty, <-chan Window, bool) {
|
||||
if sess.pty != nil {
|
||||
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
|
||||
}
|
||||
|
||||
sess.handled = true
|
||||
req.Reply(true, nil)
|
||||
|
||||
go func() {
|
||||
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() {
|
||||
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
|
||||
defer func() {
|
||||
// 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 := parseWinchRequest(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:
|
||||
// TODO: debug log
|
||||
req.Reply(false, nil)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package ssh
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
// 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
|
||||
|
||||
// PtyCallback is a hook for allowing PTY sessions.
|
||||
type PtyCallback func(ctx Context, pty 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)
|
||||
|
||||
// Window represents the size of a PTY window.
|
||||
type Window struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
// Pty represents a PTY request and configuration.
|
||||
type Pty struct {
|
||||
Term string
|
||||
Window Window
|
||||
// HELP WANTED: terminal modes!
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
package ssh
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"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, conn *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 ch.Close()
|
||||
defer dconn.Close()
|
||||
io.Copy(ch, dconn)
|
||||
}()
|
||||
go func() {
|
||||
defer ch.Close()
|
||||
defer 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
|
||||
}
|
||||
|
||||
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 {
|
||||
// TODO: log parse failure
|
||||
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 {
|
||||
// TODO: log listen failure
|
||||
return false, []byte{}
|
||||
}
|
||||
_, destPortStr, _ := net.SplitHostPort(ln.Addr().String())
|
||||
destPort, _ := strconv.Atoi(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 {
|
||||
// TODO: log accept failure
|
||||
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() {
|
||||
ch, reqs, err := conn.OpenChannel(forwardedTCPChannelType, payload)
|
||||
if err != nil {
|
||||
// TODO: log failure to open channel
|
||||
log.Println(err)
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
go gossh.DiscardRequests(reqs)
|
||||
go func() {
|
||||
defer ch.Close()
|
||||
defer c.Close()
|
||||
io.Copy(ch, c)
|
||||
}()
|
||||
go func() {
|
||||
defer ch.Close()
|
||||
defer 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 {
|
||||
// TODO: log parse failure
|
||||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
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(s []byte) (pty Pty, ok bool) {
|
||||
term, s, ok := parseString(s)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
width32, s, ok := parseUint32(s)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
height32, _, ok := parseUint32(s)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
pty = Pty{
|
||||
Term: term,
|
||||
Window: Window{
|
||||
Width: int(width32),
|
||||
Height: int(height32),
|
||||
},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseWinchRequest(s []byte) (win Window, ok bool) {
|
||||
width32, s, ok := parseUint32(s)
|
||||
if width32 < 1 {
|
||||
ok = false
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
height32, _, ok := parseUint32(s)
|
||||
if height32 < 1 {
|
||||
ok = false
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
win = Window{
|
||||
Width: int(width32),
|
||||
Height: int(height32),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseString(in []byte) (out string, rest []byte, ok bool) {
|
||||
if len(in) < 4 {
|
||||
return
|
||||
}
|
||||
length := binary.BigEndian.Uint32(in)
|
||||
if uint32(len(in)) < 4+length {
|
||||
return
|
||||
}
|
||||
out = string(in[4 : 4+length])
|
||||
rest = in[4+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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
examples/bubbletea/bubbletea
|
||||
examples/bubbletea/.ssh
|
||||
examples/git/git
|
||||
examples/git/.ssh
|
||||
examples/git/.repos
|
||||
.repos
|
||||
.ssh
|
||||
coverage.txt
|
||||
|
||||
# MacOS specific
|
||||
.DS_Store
|
|
@ -0,0 +1,34 @@
|
|||
run:
|
||||
tests: false
|
||||
|
||||
issues:
|
||||
include:
|
||||
- EXC0001
|
||||
- EXC0005
|
||||
- EXC0011
|
||||
- EXC0012
|
||||
- EXC0013
|
||||
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- bodyclose
|
||||
- dupl
|
||||
- exportloopref
|
||||
- goconst
|
||||
- godot
|
||||
- godox
|
||||
- goimports
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- ifshort
|
||||
- misspell
|
||||
- prealloc
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- whitespace
|
|
@ -0,0 +1,8 @@
|
|||
includes:
|
||||
- from_url:
|
||||
url: charmbracelet/meta/main/goreleaser-lib.yaml
|
||||
- from_url:
|
||||
url: charmbracelet/meta/main/goreleaser-announce.yaml
|
||||
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019-2021 Charmbracelet, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,142 @@
|
|||
# Wish
|
||||
|
||||
<p>
|
||||
<img style="width: 247px" src="https://stuff.charm.sh/wish/wish-header.png" alt="A nice rendering of a star, anthropomorphized somewhat by means of a smile, with the words ‘Charm Wish’ next to it">
|
||||
<br>
|
||||
<a href="https://github.com/charmbracelet/wish/releases"><img src="https://img.shields.io/github/release/charmbracelet/wish.svg" alt="Latest Release"></a>
|
||||
<a href="https://pkg.go.dev/github.com/charmbracelet/wish?tab=doc"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="GoDoc"></a>
|
||||
<a href="https://github.com/charmbracelet/wish/actions"><img src="https://github.com/charmbracelet/wish/workflows/Build/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://codecov.io/gh/charmbracelet/wish"><img alt="Codecov branch" src="https://img.shields.io/codecov/c/github/charmbracelet/wish/main.svg"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/charmbracelet/wish"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/charmbracelet/wish"></a>
|
||||
</p>
|
||||
|
||||
|
||||
Make SSH apps, just like that! 💫
|
||||
|
||||
SSH is an excellent platform to build remotely accessible applications on. It
|
||||
offers secure communication without the hassle of HTTPS certificates, it has
|
||||
user identification with SSH keys and it's accessible from anywhere with a
|
||||
terminal. Powerful protocols like Git work over SSH and you can even render
|
||||
TUIs directly over an SSH connection.
|
||||
|
||||
Wish is an SSH server with sensible defaults and a collection of middleware that
|
||||
makes building SSH apps easy. Wish is built on [gliderlabs/ssh][gliderlabs/ssh]
|
||||
and should be easy to integrate into any existing projects.
|
||||
|
||||
## What are SSH Apps?
|
||||
|
||||
Usually, when we think about SSH, we think about remote shell access into servers,
|
||||
most commonly through `openssh-server`.
|
||||
|
||||
That's a perfectly valid and probably the most common use of SSH, but it can do so much more than that.
|
||||
Just like HTTP, SMTP, FTP and others, SSH is a protocol!
|
||||
It is a cryptographic network protocol for operating network services securely over an unsecured network. [^1]
|
||||
|
||||
[^1]: https://en.wikipedia.org/wiki/Secure_Shell
|
||||
|
||||
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.
|
||||
|
||||
## Middleware
|
||||
|
||||
Wish middlewares are analogous to those in several HTTP frameworks.
|
||||
They are essentially SSH handlers that you can use to do specific tasks,
|
||||
and then call the next middleware.
|
||||
|
||||
Notice that middlewares are composed from first to last,
|
||||
which means the last one is executed first.
|
||||
|
||||
### Bubble Tea
|
||||
|
||||
The [`bubbletea`](bubbletea) middleware makes it easy to serve any
|
||||
[Bubble Tea][bubbletea] application over SSH. Each SSH session will get their own
|
||||
`tea.Program` with the SSH pty input and output connected. Client window
|
||||
dimension and resize messages are also natively handled by the `tea.Program`.
|
||||
|
||||
You can see a demo of the Wish middleware in action at: `ssh git.charm.sh`
|
||||
|
||||
### Git
|
||||
|
||||
The [`git`](git) middleware adds `git` server functionality to any ssh server.
|
||||
It supports repo creation on initial push and custom public key based auth.
|
||||
|
||||
This middleware requires that `git` is installed on the server.
|
||||
|
||||
### Logging
|
||||
|
||||
The [`logging`](logging) middleware provides basic connection logging. Connects
|
||||
are logged with the remote address, invoked command, TERM setting, window
|
||||
dimensions and if the auth was public key based. Disconnect will log the remote
|
||||
address and connection duration.
|
||||
|
||||
### Access Control
|
||||
|
||||
Not all applications will support general SSH connections. To restrict access
|
||||
to supported methods, you can use the [`activeterm`](activeterm) middleware to
|
||||
only allow connections with active terminals connected and the
|
||||
[`accesscontrol`](accesscontrol) middleware that lets you specify allowed
|
||||
commands.
|
||||
|
||||
## Default Server
|
||||
|
||||
Wish includes the ability to easily create an always authenticating default SSH
|
||||
server with automatic server key generation.
|
||||
|
||||
## Examples
|
||||
|
||||
There are examples for a standalone [Bubble Tea application](examples/bubbletea)
|
||||
and [Git server](examples/git) in the [examples](examples) folder.
|
||||
|
||||
## Apps Built With Wish
|
||||
|
||||
* [Soft Serve](https://github.com/charmbracelet/soft-serve)
|
||||
* [Wishlist](https://github.com/charmbracelet/wishlist)
|
||||
* [SSHWordle](https://github.com/davidcroda/sshwordle)
|
||||
* [clidle](https://github.com/ajeetdsouza/clidle)
|
||||
* [ssh-warm-welcome](https://git.vvvvvvaria.org/decentral1se/ssh-warm-welcome)
|
||||
|
||||
[bubbletea]: https://github.com/charmbracelet/bubbletea
|
||||
[gliderlabs/ssh]: https://github.com/gliderlabs/ssh
|
||||
|
||||
## Pro Tip
|
||||
|
||||
When building various Wish applications locally you can add the following to
|
||||
your `~/.ssh/config` to avoid having to clear out `localhost` entries in your
|
||||
`~/.ssh/known_hosts` file:
|
||||
|
||||
```
|
||||
Host localhost
|
||||
UserKnownHostsFile /dev/null
|
||||
```
|
||||
|
||||
## How it works?
|
||||
|
||||
Wish uses [gliderlabs/ssh][gliderlabs/ssh] to implement its SSH server, and
|
||||
the OpenSSH is never used nor needed — you can even uninstall it if you want to.
|
||||
|
||||
Incidentally, there's no risk of accidentally sharing a shell because there's no
|
||||
default behavior that does that on Wish.
|
||||
|
||||
###
|
||||
|
||||
## 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.sh/chat)
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/charmbracelet/wish/raw/main/LICENSE)
|
||||
|
||||
***
|
||||
|
||||
Part of [Charm](https://charm.sh).
|
||||
|
||||
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
|
||||
|
||||
Charm热爱开源 • Charm loves open source
|
|
@ -0,0 +1,41 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/ssh"
|
||||
"github.com/charmbracelet/wish"
|
||||
)
|
||||
|
||||
// Middleware provides basic connection logging. Connects are logged with the
|
||||
// remote address, invoked command, TERM setting, window dimensions and if the
|
||||
// auth was public key based. Disconnect will log the remote address and
|
||||
// connection duration.
|
||||
//
|
||||
// The logger is set to the std default logger.
|
||||
func Middleware() wish.Middleware {
|
||||
return MiddlewareWithLogger(log.Default())
|
||||
}
|
||||
|
||||
// Logger is the interface that wraps the basic Log method.
|
||||
type Logger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// MiddlewareWithLogger provides basic connection logging. Connects are logged with the
|
||||
// remote address, invoked command, TERM setting, window dimensions and if the
|
||||
// auth was public key based. Disconnect will log the remote address and
|
||||
// connection duration.
|
||||
func MiddlewareWithLogger(l Logger) wish.Middleware {
|
||||
return func(sh ssh.Handler) ssh.Handler {
|
||||
return func(s ssh.Session) {
|
||||
ct := time.Now()
|
||||
hpk := s.PublicKey() != nil
|
||||
pty, _, _ := s.Pty()
|
||||
l.Printf("%s connect %s %v %v %s %v %v\n", s.User(), s.RemoteAddr().String(), hpk, s.Command(), pty.Term, pty.Window.Width, pty.Window.Height)
|
||||
sh(s)
|
||||
l.Printf("%s disconnect %s\n", s.RemoteAddr().String(), time.Since(ct))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
package wish
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/keygen"
|
||||
"github.com/charmbracelet/ssh"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// WithAddress returns an ssh.Option that sets the address to listen on.
|
||||
func WithAddress(addr string) ssh.Option {
|
||||
return func(s *ssh.Server) error {
|
||||
s.Addr = addr
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithVersion returns an ssh.Option that sets the server version.
|
||||
func WithVersion(version string) ssh.Option {
|
||||
return func(s *ssh.Server) error {
|
||||
s.Version = version
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithMiddleware composes the provided Middleware and return a ssh.Option.
|
||||
// This useful if you manually create an ssh.Server and want to set the
|
||||
// Server.Handler.
|
||||
//
|
||||
// Notice that middlewares are composed from first to last, which means the last one is executed first.
|
||||
func WithMiddleware(mw ...Middleware) ssh.Option {
|
||||
return func(s *ssh.Server) error {
|
||||
h := func(s ssh.Session) {}
|
||||
for _, m := range mw {
|
||||
h = m(h)
|
||||
}
|
||||
s.Handler = h
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithHostKeyFile returns an ssh.Option that sets the path to the private.
|
||||
func WithHostKeyPath(path string) ssh.Option {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
dir, f := filepath.Split(path)
|
||||
n := strings.TrimSuffix(f, "_ed25519")
|
||||
_, err := keygen.NewWithWrite(filepath.Join(dir, n), nil, keygen.Ed25519)
|
||||
if err != nil {
|
||||
return func(*ssh.Server) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
path = filepath.Join(dir, n+"_ed25519")
|
||||
}
|
||||
return ssh.HostKeyFile(path)
|
||||
}
|
||||
|
||||
// WithHostKeyPEM returns an ssh.Option that sets the host key from a PEM block.
|
||||
func WithHostKeyPEM(pem []byte) ssh.Option {
|
||||
return ssh.HostKeyPEM(pem)
|
||||
}
|
||||
|
||||
// WithAuthorizedKeys allows to use a SSH authorized_keys file to allowlist users.
|
||||
func WithAuthorizedKeys(path string) ssh.Option {
|
||||
return func(s *ssh.Server) error {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return err
|
||||
}
|
||||
return WithPublicKeyAuth(func(_ ssh.Context, key ssh.PublicKey) bool {
|
||||
return isAuthorized(path, func(k ssh.PublicKey) bool {
|
||||
return ssh.KeysEqual(key, k)
|
||||
})
|
||||
})(s)
|
||||
}
|
||||
}
|
||||
|
||||
// WithTrustedUserCAKeys authorize certificates that are signed with the given
|
||||
// Certificate Authority public key, and are valid.
|
||||
// Analogous to the TrustedUserCAKeys OpenSSH option.
|
||||
func WithTrustedUserCAKeys(path string) ssh.Option {
|
||||
return func(s *ssh.Server) error {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return err
|
||||
}
|
||||
return WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
cert, ok := key.(*gossh.Certificate)
|
||||
if !ok {
|
||||
// not a certificate...
|
||||
return false
|
||||
}
|
||||
|
||||
return isAuthorized(path, func(k ssh.PublicKey) bool {
|
||||
checker := &gossh.CertChecker{
|
||||
IsUserAuthority: func(auth gossh.PublicKey) bool {
|
||||
// its a cert signed by one of the CAs
|
||||
return bytes.Equal(auth.Marshal(), k.Marshal())
|
||||
},
|
||||
}
|
||||
|
||||
if !checker.IsUserAuthority(cert.SignatureKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
if err := checker.CheckCert(ctx.User(), cert); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
})(s)
|
||||
}
|
||||
}
|
||||
|
||||
func isAuthorized(path string, checker func(k ssh.PublicKey) bool) bool {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Printf("failed to parse %q: %s", path, err)
|
||||
return false
|
||||
}
|
||||
defer f.Close() // nolint: errcheck
|
||||
|
||||
rd := bufio.NewReader(f)
|
||||
for {
|
||||
line, _, err := rd.ReadLine()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
log.Printf("failed to parse %q: %s", path, err)
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(string(line)) == "" {
|
||||
continue
|
||||
}
|
||||
if bytes.HasPrefix(line, []byte{'#'}) {
|
||||
continue
|
||||
}
|
||||
upk, _, _, _, err := ssh.ParseAuthorizedKey(line)
|
||||
if err != nil {
|
||||
log.Printf("failed to parse %q: %s", path, err)
|
||||
return false
|
||||
}
|
||||
if checker(upk) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WithPublicKeyAuth returns an ssh.Option that sets the public key auth handler.
|
||||
func WithPublicKeyAuth(h ssh.PublicKeyHandler) ssh.Option {
|
||||
return ssh.PublicKeyAuth(h)
|
||||
}
|
||||
|
||||
// WithPasswordAuth returns an ssh.Option that sets the password auth handler.
|
||||
func WithPasswordAuth(p ssh.PasswordHandler) ssh.Option {
|
||||
return ssh.PasswordAuth(p)
|
||||
}
|
||||
|
||||
// WithKeyboardInteractiveAuth returns an ssh.Option that sets the keyboard interactive auth handler.
|
||||
func WithKeyboardInteractiveAuth(h ssh.KeyboardInteractiveHandler) ssh.Option {
|
||||
return ssh.KeyboardInteractiveAuth(h)
|
||||
}
|
||||
|
||||
// WithIdleTimeout returns an ssh.Option that sets the connection's idle timeout.
|
||||
func WithIdleTimeout(d time.Duration) ssh.Option {
|
||||
return func(s *ssh.Server) error {
|
||||
s.IdleTimeout = d
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxTimeout returns an ssh.Option that sets the connection's absolute timeout.
|
||||
func WithMaxTimeout(d time.Duration) ssh.Option {
|
||||
return func(s *ssh.Server) error {
|
||||
s.MaxTimeout = d
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package wish
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/charmbracelet/keygen"
|
||||
"github.com/charmbracelet/ssh"
|
||||
)
|
||||
|
||||
// Middleware is a function that takes an ssh.Handler and returns an
|
||||
// ssh.Handler. Implementations should call the provided handler argument.
|
||||
type Middleware func(ssh.Handler) ssh.Handler
|
||||
|
||||
// NewServer is returns a default SSH server with the provided Middleware. A
|
||||
// new SSH key pair of type ed25519 will be created if one does not exist. By
|
||||
// default this server will accept all incoming connections, password and
|
||||
// public key.
|
||||
func NewServer(ops ...ssh.Option) (*ssh.Server, error) {
|
||||
s := &ssh.Server{}
|
||||
// Some sensible defaults
|
||||
s.Version = "OpenSSH_7.6p1"
|
||||
for _, op := range ops {
|
||||
if err := s.SetOption(op); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(s.HostSigners) == 0 {
|
||||
k, err := keygen.New("", nil, keygen.Ed25519)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = s.SetOption(WithHostKeyPEM(k.PrivateKeyPEM()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Fatal prints to the given session's STDERR and exits 1.
|
||||
func Fatal(s ssh.Session, v ...interface{}) {
|
||||
Error(s, v...)
|
||||
_ = s.Exit(1)
|
||||
_ = s.Close()
|
||||
}
|
||||
|
||||
// Fatalf formats according to the given format, prints to the session's STDERR
|
||||
// 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{}) {
|
||||
Errorf(s, f, v...)
|
||||
_ = s.Exit(1)
|
||||
_ = s.Close()
|
||||
}
|
||||
|
||||
// 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{}) {
|
||||
Errorln(s, v...)
|
||||
Errorf(s, "\r")
|
||||
_ = s.Exit(1)
|
||||
_ = s.Close()
|
||||
}
|
||||
|
||||
// Error prints the given error the the session's STDERR.
|
||||
func Error(s ssh.Session, v ...interface{}) {
|
||||
_, _ = 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{}) {
|
||||
_, _ = fmt.Fprintf(s.Stderr(), f, v...)
|
||||
}
|
||||
|
||||
// Errorf formats according to the default format and prints to the session's STDERR.
|
||||
func Errorln(s ssh.Session, v ...interface{}) {
|
||||
_, _ = fmt.Fprintln(s.Stderr(), v...)
|
||||
}
|
||||
|
||||
// Print writes to the session's STDOUT followed.
|
||||
func Print(s ssh.Session, v ...interface{}) {
|
||||
_, _ = 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{}) {
|
||||
_, _ = 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{}) {
|
||||
_, _ = fmt.Fprintln(s, v...)
|
||||
}
|
||||
|
||||
// WriteString writes the given string to the session's STDOUT.
|
||||
func WriteString(s ssh.Session, v string) (int, error) {
|
||||
return io.WriteString(s, v)
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
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 [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
|
@ -0,0 +1,2 @@
|
|||
go-shlex is a simple lexer for go that supports shell-style quoting,
|
||||
commenting, and escaping.
|
|
@ -0,0 +1,416 @@
|
|||
/*
|
||||
Copyright 2012 Google Inc. All Rights Reserved.
|
||||
|
||||
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 shlex implements a simple lexer which splits input in to tokens using
|
||||
shell-style rules for quoting and commenting.
|
||||
|
||||
The basic use case uses the default ASCII lexer to split a string into sub-strings:
|
||||
|
||||
shlex.Split("one \"two three\" four") -> []string{"one", "two three", "four"}
|
||||
|
||||
To process a stream of strings:
|
||||
|
||||
l := NewLexer(os.Stdin)
|
||||
for ; token, err := l.Next(); err != nil {
|
||||
// process token
|
||||
}
|
||||
|
||||
To access the raw token stream (which includes tokens for comments):
|
||||
|
||||
t := NewTokenizer(os.Stdin)
|
||||
for ; token, err := t.Next(); err != nil {
|
||||
// process token
|
||||
}
|
||||
|
||||
*/
|
||||
package shlex
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TokenType is a top-level token classification: A word, space, comment, unknown.
|
||||
type TokenType int
|
||||
|
||||
// runeTokenClass is the type of a UTF-8 character classification: A quote, space, escape.
|
||||
type runeTokenClass int
|
||||
|
||||
// the internal state used by the lexer state machine
|
||||
type lexerState int
|
||||
|
||||
// Token is a (type, value) pair representing a lexographical token.
|
||||
type Token struct {
|
||||
tokenType TokenType
|
||||
value string
|
||||
}
|
||||
|
||||
// Equal reports whether tokens a, and b, are equal.
|
||||
// Two tokens are equal if both their types and values are equal. A nil token can
|
||||
// never be equal to another token.
|
||||
func (a *Token) Equal(b *Token) bool {
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
if a.tokenType != b.tokenType {
|
||||
return false
|
||||
}
|
||||
return a.value == b.value
|
||||
}
|
||||
|
||||
// Named classes of UTF-8 runes
|
||||
const (
|
||||
spaceRunes = " \t\r\n"
|
||||
escapingQuoteRunes = `"`
|
||||
nonEscapingQuoteRunes = "'"
|
||||
escapeRunes = `\`
|
||||
commentRunes = "#"
|
||||
)
|
||||
|
||||
// Classes of rune token
|
||||
const (
|
||||
unknownRuneClass runeTokenClass = iota
|
||||
spaceRuneClass
|
||||
escapingQuoteRuneClass
|
||||
nonEscapingQuoteRuneClass
|
||||
escapeRuneClass
|
||||
commentRuneClass
|
||||
eofRuneClass
|
||||
)
|
||||
|
||||
// Classes of lexographic token
|
||||
const (
|
||||
UnknownToken TokenType = iota
|
||||
WordToken
|
||||
SpaceToken
|
||||
CommentToken
|
||||
)
|
||||
|
||||
// Lexer state machine states
|
||||
const (
|
||||
startState lexerState = iota // no runes have been seen
|
||||
inWordState // processing regular runes in a word
|
||||
escapingState // we have just consumed an escape rune; the next rune is literal
|
||||
escapingQuotedState // we have just consumed an escape rune within a quoted string
|
||||
quotingEscapingState // we are within a quoted string that supports escaping ("...")
|
||||
quotingState // we are within a string that does not support escaping ('...')
|
||||
commentState // we are within a comment (everything following an unquoted or unescaped #
|
||||
)
|
||||
|
||||
// tokenClassifier is used for classifying rune characters.
|
||||
type tokenClassifier map[rune]runeTokenClass
|
||||
|
||||
func (typeMap tokenClassifier) addRuneClass(runes string, tokenType runeTokenClass) {
|
||||
for _, runeChar := range runes {
|
||||
typeMap[runeChar] = tokenType
|
||||
}
|
||||
}
|
||||
|
||||
// newDefaultClassifier creates a new classifier for ASCII characters.
|
||||
func newDefaultClassifier() tokenClassifier {
|
||||
t := tokenClassifier{}
|
||||
t.addRuneClass(spaceRunes, spaceRuneClass)
|
||||
t.addRuneClass(escapingQuoteRunes, escapingQuoteRuneClass)
|
||||
t.addRuneClass(nonEscapingQuoteRunes, nonEscapingQuoteRuneClass)
|
||||
t.addRuneClass(escapeRunes, escapeRuneClass)
|
||||
t.addRuneClass(commentRunes, commentRuneClass)
|
||||
return t
|
||||
}
|
||||
|
||||
// ClassifyRune classifiees a rune
|
||||
func (t tokenClassifier) ClassifyRune(runeVal rune) runeTokenClass {
|
||||
return t[runeVal]
|
||||
}
|
||||
|
||||
// Lexer turns an input stream into a sequence of tokens. Whitespace and comments are skipped.
|
||||
type Lexer Tokenizer
|
||||
|
||||
// NewLexer creates a new lexer from an input stream.
|
||||
func NewLexer(r io.Reader) *Lexer {
|
||||
|
||||
return (*Lexer)(NewTokenizer(r))
|
||||
}
|
||||
|
||||
// Next returns the next word, or an error. If there are no more words,
|
||||
// the error will be io.EOF.
|
||||
func (l *Lexer) Next() (string, error) {
|
||||
for {
|
||||
token, err := (*Tokenizer)(l).Next()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch token.tokenType {
|
||||
case WordToken:
|
||||
return token.value, nil
|
||||
case CommentToken:
|
||||
// skip comments
|
||||
default:
|
||||
return "", fmt.Errorf("Unknown token type: %v", token.tokenType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tokenizer turns an input stream into a sequence of typed tokens
|
||||
type Tokenizer struct {
|
||||
input bufio.Reader
|
||||
classifier tokenClassifier
|
||||
}
|
||||
|
||||
// NewTokenizer creates a new tokenizer from an input stream.
|
||||
func NewTokenizer(r io.Reader) *Tokenizer {
|
||||
input := bufio.NewReader(r)
|
||||
classifier := newDefaultClassifier()
|
||||
return &Tokenizer{
|
||||
input: *input,
|
||||
classifier: classifier}
|
||||
}
|
||||
|
||||
// scanStream scans the stream for the next token using the internal state machine.
|
||||
// It will panic if it encounters a rune which it does not know how to handle.
|
||||
func (t *Tokenizer) scanStream() (*Token, error) {
|
||||
state := startState
|
||||
var tokenType TokenType
|
||||
var value []rune
|
||||
var nextRune rune
|
||||
var nextRuneType runeTokenClass
|
||||
var err error
|
||||
|
||||
for {
|
||||
nextRune, _, err = t.input.ReadRune()
|
||||
nextRuneType = t.classifier.ClassifyRune(nextRune)
|
||||
|
||||
if err == io.EOF {
|
||||
nextRuneType = eofRuneClass
|
||||
err = nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch state {
|
||||
case startState: // no runes read yet
|
||||
{
|
||||
switch nextRuneType {
|
||||
case eofRuneClass:
|
||||
{
|
||||
return nil, io.EOF
|
||||
}
|
||||
case spaceRuneClass:
|
||||
{
|
||||
}
|
||||
case escapingQuoteRuneClass:
|
||||
{
|
||||
tokenType = WordToken
|
||||
state = quotingEscapingState
|
||||
}
|
||||
case nonEscapingQuoteRuneClass:
|
||||
{
|
||||
tokenType = WordToken
|
||||
state = quotingState
|
||||
}
|
||||
case escapeRuneClass:
|
||||
{
|
||||
tokenType = WordToken
|
||||
state = escapingState
|
||||
}
|
||||
case commentRuneClass:
|
||||
{
|
||||
tokenType = CommentToken
|
||||
state = commentState
|
||||
}
|
||||
default:
|
||||
{
|
||||
tokenType = WordToken
|
||||
value = append(value, nextRune)
|
||||
state = inWordState
|
||||
}
|
||||
}
|
||||
}
|
||||
case inWordState: // in a regular word
|
||||
{
|
||||
switch nextRuneType {
|
||||
case eofRuneClass:
|
||||
{
|
||||
token := &Token{
|
||||
tokenType: tokenType,
|
||||
value: string(value)}
|
||||
return token, err
|
||||
}
|
||||
case spaceRuneClass:
|
||||
{
|
||||
token := &Token{
|
||||
tokenType: tokenType,
|
||||
value: string(value)}
|
||||
return token, err
|
||||
}
|
||||
case escapingQuoteRuneClass:
|
||||
{
|
||||
state = quotingEscapingState
|
||||
}
|
||||
case nonEscapingQuoteRuneClass:
|
||||
{
|
||||
state = quotingState
|
||||
}
|
||||
case escapeRuneClass:
|
||||
{
|
||||
state = escapingState
|
||||
}
|
||||
default:
|
||||
{
|
||||
value = append(value, nextRune)
|
||||
}
|
||||
}
|
||||
}
|
||||
case escapingState: // the rune after an escape character
|
||||
{
|
||||
switch nextRuneType {
|
||||
case eofRuneClass:
|
||||
{
|
||||
err = fmt.Errorf("EOF found after escape character")
|
||||
token := &Token{
|
||||
tokenType: tokenType,
|
||||
value: string(value)}
|
||||
return token, err
|
||||
}
|
||||
default:
|
||||
{
|
||||
state = inWordState
|
||||
value = append(value, nextRune)
|
||||
}
|
||||
}
|
||||
}
|
||||
case escapingQuotedState: // the next rune after an escape character, in double quotes
|
||||
{
|
||||
switch nextRuneType {
|
||||
case eofRuneClass:
|
||||
{
|
||||
err = fmt.Errorf("EOF found after escape character")
|
||||
token := &Token{
|
||||
tokenType: tokenType,
|
||||
value: string(value)}
|
||||
return token, err
|
||||
}
|
||||
default:
|
||||
{
|
||||
state = quotingEscapingState
|
||||
value = append(value, nextRune)
|
||||
}
|
||||
}
|
||||
}
|
||||
case quotingEscapingState: // in escaping double quotes
|
||||
{
|
||||
switch nextRuneType {
|
||||
case eofRuneClass:
|
||||
{
|
||||
err = fmt.Errorf("EOF found when expecting closing quote")
|
||||
token := &Token{
|
||||
tokenType: tokenType,
|
||||
value: string(value)}
|
||||
return token, err
|
||||
}
|
||||
case escapingQuoteRuneClass:
|
||||
{
|
||||
state = inWordState
|
||||
}
|
||||
case escapeRuneClass:
|
||||
{
|
||||
state = escapingQuotedState
|
||||
}
|
||||
default:
|
||||
{
|
||||
value = append(value, nextRune)
|
||||
}
|
||||
}
|
||||
}
|
||||
case quotingState: // in non-escaping single quotes
|
||||
{
|
||||
switch nextRuneType {
|
||||
case eofRuneClass:
|
||||
{
|
||||
err = fmt.Errorf("EOF found when expecting closing quote")
|
||||
token := &Token{
|
||||
tokenType: tokenType,
|
||||
value: string(value)}
|
||||
return token, err
|
||||
}
|
||||
case nonEscapingQuoteRuneClass:
|
||||
{
|
||||
state = inWordState
|
||||
}
|
||||
default:
|
||||
{
|
||||
value = append(value, nextRune)
|
||||
}
|
||||
}
|
||||
}
|
||||
case commentState: // in a comment
|
||||
{
|
||||
switch nextRuneType {
|
||||
case eofRuneClass:
|
||||
{
|
||||
token := &Token{
|
||||
tokenType: tokenType,
|
||||
value: string(value)}
|
||||
return token, err
|
||||
}
|
||||
case spaceRuneClass:
|
||||
{
|
||||
if nextRune == '\n' {
|
||||
state = startState
|
||||
token := &Token{
|
||||
tokenType: tokenType,
|
||||
value: string(value)}
|
||||
return token, err
|
||||
} else {
|
||||
value = append(value, nextRune)
|
||||
}
|
||||
}
|
||||
default:
|
||||
{
|
||||
value = append(value, nextRune)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
{
|
||||
return nil, fmt.Errorf("Unexpected state: %v", state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next returns the next token in the stream.
|
||||
func (t *Tokenizer) Next() (*Token, error) {
|
||||
return t.scanStream()
|
||||
}
|
||||
|
||||
// Split partitions a string into a slice of strings.
|
||||
func Split(s string) ([]string, error) {
|
||||
l := NewLexer(strings.NewReader(s))
|
||||
subStrings := make([]string, 0)
|
||||
for {
|
||||
word, err := l.Next()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return subStrings, nil
|
||||
}
|
||||
return subStrings, err
|
||||
}
|
||||
subStrings = append(subStrings, word)
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.13.x
|
||||
- tip
|
||||
|
||||
before_install:
|
||||
- go get -t -v ./...
|
||||
|
||||
script:
|
||||
- go generate
|
||||
- git diff --cached --exit-code
|
||||
- ./go.test.sh
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
|
@ -1,7 +1,7 @@
|
|||
go-runewidth
|
||||
============
|
||||
|
||||
[![Build Status](https://travis-ci.org/mattn/go-runewidth.png?branch=master)](https://travis-ci.org/mattn/go-runewidth)
|
||||
[![Build Status](https://github.com/mattn/go-runewidth/workflows/test/badge.svg?branch=master)](https://github.com/mattn/go-runewidth/actions?query=workflow%3Atest)
|
||||
[![Codecov](https://codecov.io/gh/mattn/go-runewidth/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-runewidth)
|
||||
[![GoDoc](https://godoc.org/github.com/mattn/go-runewidth?status.svg)](http://godoc.org/github.com/mattn/go-runewidth)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-runewidth)](https://goreportcard.com/report/github.com/mattn/go-runewidth)
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
echo "" > coverage.txt
|
||||
|
||||
for d in $(go list ./... | grep -v vendor); do
|
||||
go test -race -coverprofile=profile.out -covermode=atomic "$d"
|
||||
if [ -f profile.out ]; then
|
||||
cat profile.out >> coverage.txt
|
||||
rm profile.out
|
||||
fi
|
||||
done
|
|
@ -2,6 +2,7 @@ package runewidth
|
|||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
@ -34,7 +35,13 @@ func handleEnv() {
|
|||
EastAsianWidth = env == "1"
|
||||
}
|
||||
// update DefaultCondition
|
||||
DefaultCondition.EastAsianWidth = EastAsianWidth
|
||||
if DefaultCondition.EastAsianWidth != EastAsianWidth {
|
||||
DefaultCondition.EastAsianWidth = EastAsianWidth
|
||||
if len(DefaultCondition.combinedLut) > 0 {
|
||||
DefaultCondition.combinedLut = DefaultCondition.combinedLut[:0]
|
||||
CreateLUT()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type interval struct {
|
||||
|
@ -89,6 +96,7 @@ var nonprint = table{
|
|||
|
||||
// Condition have flag EastAsianWidth whether the current locale is CJK or not.
|
||||
type Condition struct {
|
||||
combinedLut []byte
|
||||
EastAsianWidth bool
|
||||
StrictEmojiNeutral bool
|
||||
}
|
||||
|
@ -104,10 +112,16 @@ func NewCondition() *Condition {
|
|||
// RuneWidth returns the number of cells in r.
|
||||
// See http://www.unicode.org/reports/tr11/
|
||||
func (c *Condition) RuneWidth(r rune) int {
|
||||
if r < 0 || r > 0x10FFFF {
|
||||
return 0
|
||||
}
|
||||
if len(c.combinedLut) > 0 {
|
||||
return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
|
||||
}
|
||||
// optimized version, verified by TestRuneWidthChecksums()
|
||||
if !c.EastAsianWidth {
|
||||
switch {
|
||||
case r < 0x20 || r > 0x10FFFF:
|
||||
case r < 0x20:
|
||||
return 0
|
||||
case (r >= 0x7F && r <= 0x9F) || r == 0xAD: // nonprint
|
||||
return 0
|
||||
|
@ -124,7 +138,7 @@ func (c *Condition) RuneWidth(r rune) int {
|
|||
}
|
||||
} else {
|
||||
switch {
|
||||
case r < 0 || r > 0x10FFFF || inTables(r, nonprint, combining):
|
||||
case inTables(r, nonprint, combining):
|
||||
return 0
|
||||
case inTable(r, narrow):
|
||||
return 1
|
||||
|
@ -138,6 +152,27 @@ func (c *Condition) RuneWidth(r rune) int {
|
|||
}
|
||||
}
|
||||
|
||||
// CreateLUT will create an in-memory lookup table of 557056 bytes for faster operation.
|
||||
// This should not be called concurrently with other operations on c.
|
||||
// If options in c is changed, CreateLUT should be called again.
|
||||
func (c *Condition) CreateLUT() {
|
||||
const max = 0x110000
|
||||
lut := c.combinedLut
|
||||
if len(c.combinedLut) != 0 {
|
||||
// Remove so we don't use it.
|
||||
c.combinedLut = nil
|
||||
} else {
|
||||
lut = make([]byte, max/2)
|
||||
}
|
||||
for i := range lut {
|
||||
i32 := int32(i * 2)
|
||||
x0 := c.RuneWidth(i32)
|
||||
x1 := c.RuneWidth(i32 + 1)
|
||||
lut[i] = uint8(x0) | uint8(x1)<<4
|
||||
}
|
||||
c.combinedLut = lut
|
||||
}
|
||||
|
||||
// StringWidth return width as you can see
|
||||
func (c *Condition) StringWidth(s string) (width int) {
|
||||
g := uniseg.NewGraphemes(s)
|
||||
|
@ -180,11 +215,47 @@ func (c *Condition) Truncate(s string, w int, tail string) string {
|
|||
return s[:pos] + tail
|
||||
}
|
||||
|
||||
// TruncateLeft cuts w cells from the beginning of the `s`.
|
||||
func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
|
||||
if c.StringWidth(s) <= w {
|
||||
return prefix
|
||||
}
|
||||
|
||||
var width int
|
||||
pos := len(s)
|
||||
|
||||
g := uniseg.NewGraphemes(s)
|
||||
for g.Next() {
|
||||
var chWidth int
|
||||
for _, r := range g.Runes() {
|
||||
chWidth = c.RuneWidth(r)
|
||||
if chWidth > 0 {
|
||||
break // See StringWidth() for details.
|
||||
}
|
||||
}
|
||||
|
||||
if width+chWidth > w {
|
||||
if width < w {
|
||||
_, pos = g.Positions()
|
||||
prefix += strings.Repeat(" ", width+chWidth-w)
|
||||
} else {
|
||||
pos, _ = g.Positions()
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
width += chWidth
|
||||
}
|
||||
|
||||
return prefix + s[pos:]
|
||||
}
|
||||
|
||||
// Wrap return string wrapped with w cells
|
||||
func (c *Condition) Wrap(s string, w int) string {
|
||||
width := 0
|
||||
out := ""
|
||||
for _, r := range []rune(s) {
|
||||
for _, r := range s {
|
||||
cw := c.RuneWidth(r)
|
||||
if r == '\n' {
|
||||
out += string(r)
|
||||
|
@ -257,6 +328,11 @@ func Truncate(s string, w int, tail string) string {
|
|||
return DefaultCondition.Truncate(s, w, tail)
|
||||
}
|
||||
|
||||
// TruncateLeft cuts w cells from the beginning of the `s`.
|
||||
func TruncateLeft(s string, w int, prefix string) string {
|
||||
return DefaultCondition.TruncateLeft(s, w, prefix)
|
||||
}
|
||||
|
||||
// Wrap return string wrapped with w cells
|
||||
func Wrap(s string, w int) string {
|
||||
return DefaultCondition.Wrap(s, w)
|
||||
|
@ -271,3 +347,12 @@ func FillLeft(s string, w int) string {
|
|||
func FillRight(s string, w int) string {
|
||||
return DefaultCondition.FillRight(s, w)
|
||||
}
|
||||
|
||||
// CreateLUT will create an in-memory lookup table of 557055 bytes for faster operation.
|
||||
// This should not be called concurrently with other operations.
|
||||
func CreateLUT() {
|
||||
if len(DefaultCondition.combinedLut) > 0 {
|
||||
return
|
||||
}
|
||||
DefaultCondition.CreateLUT()
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
//go:build appengine
|
||||
// +build appengine
|
||||
|
||||
package runewidth
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// +build js
|
||||
// +build !appengine
|
||||
//go:build js && !appengine
|
||||
// +build js,!appengine
|
||||
|
||||
package runewidth
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// +build !windows
|
||||
// +build !js
|
||||
// +build !appengine
|
||||
//go:build !windows && !js && !appengine
|
||||
// +build !windows,!js,!appengine
|
||||
|
||||
package runewidth
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// +build windows
|
||||
// +build !appengine
|
||||
//go:build windows && !appengine
|
||||
// +build windows,!appengine
|
||||
|
||||
package runewidth
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Mitchell Hashimoto
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
|
@ -0,0 +1,14 @@
|
|||
# go-homedir
|
||||
|
||||
This is a Go library for detecting the user's home directory without
|
||||
the use of cgo, so the library can be used in cross-compilation environments.
|
||||
|
||||
Usage is incredibly simple, just call `homedir.Dir()` to get the home directory
|
||||
for a user, and `homedir.Expand()` to expand the `~` in a path to the home
|
||||
directory.
|
||||
|
||||
**Why not just use `os/user`?** The built-in `os/user` package requires
|
||||
cgo on Darwin systems. This means that any Go code that uses that package
|
||||
cannot cross compile. But 99% of the time the use for `os/user` is just to
|
||||
retrieve the home directory, which we can do for the current user without
|
||||
cgo. This library does that, enabling cross-compilation.
|
|
@ -0,0 +1,167 @@
|
|||
package homedir
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// DisableCache will disable caching of the home directory. Caching is enabled
|
||||
// by default.
|
||||
var DisableCache bool
|
||||
|
||||
var homedirCache string
|
||||
var cacheLock sync.RWMutex
|
||||
|
||||
// Dir returns the home directory for the executing user.
|
||||
//
|
||||
// This uses an OS-specific method for discovering the home directory.
|
||||
// An error is returned if a home directory cannot be detected.
|
||||
func Dir() (string, error) {
|
||||
if !DisableCache {
|
||||
cacheLock.RLock()
|
||||
cached := homedirCache
|
||||
cacheLock.RUnlock()
|
||||
if cached != "" {
|
||||
return cached, nil
|
||||
}
|
||||
}
|
||||
|
||||
cacheLock.Lock()
|
||||
defer cacheLock.Unlock()
|
||||
|
||||
var result string
|
||||
var err error
|
||||
if runtime.GOOS == "windows" {
|
||||
result, err = dirWindows()
|
||||
} else {
|
||||
// Unix-like system, so just assume Unix
|
||||
result, err = dirUnix()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
homedirCache = result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Expand expands the path to include the home directory if the path
|
||||
// is prefixed with `~`. If it isn't prefixed with `~`, the path is
|
||||
// returned as-is.
|
||||
func Expand(path string) (string, error) {
|
||||
if len(path) == 0 {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
if path[0] != '~' {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
if len(path) > 1 && path[1] != '/' && path[1] != '\\' {
|
||||
return "", errors.New("cannot expand user-specific home dir")
|
||||
}
|
||||
|
||||
dir, err := Dir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(dir, path[1:]), nil
|
||||
}
|
||||
|
||||
// Reset clears the cache, forcing the next call to Dir to re-detect
|
||||
// the home directory. This generally never has to be called, but can be
|
||||
// useful in tests if you're modifying the home directory via the HOME
|
||||
// env var or something.
|
||||
func Reset() {
|
||||
cacheLock.Lock()
|
||||
defer cacheLock.Unlock()
|
||||
homedirCache = ""
|
||||
}
|
||||
|
||||
func dirUnix() (string, error) {
|
||||
homeEnv := "HOME"
|
||||
if runtime.GOOS == "plan9" {
|
||||
// On plan9, env vars are lowercase.
|
||||
homeEnv = "home"
|
||||
}
|
||||
|
||||
// First prefer the HOME environmental variable
|
||||
if home := os.Getenv(homeEnv); home != "" {
|
||||
return home, nil
|
||||
}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
|
||||
// If that fails, try OS specific commands
|
||||
if runtime.GOOS == "darwin" {
|
||||
cmd := exec.Command("sh", "-c", `dscl -q . -read /Users/"$(whoami)" NFSHomeDirectory | sed 's/^[^ ]*: //'`)
|
||||
cmd.Stdout = &stdout
|
||||
if err := cmd.Run(); err == nil {
|
||||
result := strings.TrimSpace(stdout.String())
|
||||
if result != "" {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid()))
|
||||
cmd.Stdout = &stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
// If the error is ErrNotFound, we ignore it. Otherwise, return it.
|
||||
if err != exec.ErrNotFound {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
if passwd := strings.TrimSpace(stdout.String()); passwd != "" {
|
||||
// username:password:uid:gid:gecos:home:shell
|
||||
passwdParts := strings.SplitN(passwd, ":", 7)
|
||||
if len(passwdParts) > 5 {
|
||||
return passwdParts[5], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all else fails, try the shell
|
||||
stdout.Reset()
|
||||
cmd := exec.Command("sh", "-c", "cd && pwd")
|
||||
cmd.Stdout = &stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result := strings.TrimSpace(stdout.String())
|
||||
if result == "" {
|
||||
return "", errors.New("blank output when reading home directory")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func dirWindows() (string, error) {
|
||||
// First prefer the HOME environmental variable
|
||||
if home := os.Getenv("HOME"); home != "" {
|
||||
return home, nil
|
||||
}
|
||||
|
||||
// Prefer standard environment variable USERPROFILE
|
||||
if home := os.Getenv("USERPROFILE"); home != "" {
|
||||
return home, nil
|
||||
}
|
||||
|
||||
drive := os.Getenv("HOMEDRIVE")
|
||||
path := os.Getenv("HOMEPATH")
|
||||
home := drive + path
|
||||
if drive == "" || path == "" {
|
||||
return "", errors.New("HOMEDRIVE, HOMEPATH, or USERPROFILE are blank")
|
||||
}
|
||||
|
||||
return home, nil
|
||||
}
|
|
@ -31,17 +31,15 @@ color conversions.
|
|||
go get github.com/muesli/termenv
|
||||
```
|
||||
|
||||
## Query Terminal Support
|
||||
|
||||
`termenv` can query the terminal it is running in, so you can safely use
|
||||
advanced features, like RGB colors. `ColorProfile` returns the color profile
|
||||
supported by the terminal:
|
||||
## Usage
|
||||
|
||||
```go
|
||||
profile := termenv.ColorProfile()
|
||||
output := termenv.NewOutput(os.Stdout)
|
||||
```
|
||||
|
||||
This returns one of the supported color profiles:
|
||||
`termenv` queries the terminal's capabilities it is running in, so you can
|
||||
safely use advanced features, like RGB colors or ANSI styles. `output.Profile`
|
||||
returns the supported profile:
|
||||
|
||||
- `termenv.Ascii` - no ANSI support detected, ASCII only
|
||||
- `termenv.ANSI` - 16 color ANSI support
|
||||
|
@ -57,39 +55,46 @@ app is running in a light- or dark-themed environment:
|
|||
|
||||
```go
|
||||
// Returns terminal's foreground color
|
||||
color := termenv.ForegroundColor()
|
||||
color := output.ForegroundColor()
|
||||
|
||||
// Returns terminal's background color
|
||||
color := termenv.BackgroundColor()
|
||||
color := output.BackgroundColor()
|
||||
|
||||
// Returns whether terminal uses a dark-ish background
|
||||
darkTheme := termenv.HasDarkBackground()
|
||||
darkTheme := output.HasDarkBackground()
|
||||
```
|
||||
|
||||
### Manual Profile Selection
|
||||
|
||||
If you don't want to rely on the automatic detection, you can manually select
|
||||
the profile you want to use:
|
||||
|
||||
```go
|
||||
output := termenv.NewOutput(os.Stdout, termenv.WithProfile(termenv.TrueColor))
|
||||
```
|
||||
|
||||
## Colors
|
||||
|
||||
`termenv` supports multiple color profiles: ANSI (16 colors), ANSI Extended
|
||||
(256 colors), and TrueColor (24-bit RGB). Colors will automatically be degraded
|
||||
to the best matching available color in the desired profile:
|
||||
`termenv` supports multiple color profiles: Ascii (black & white only),
|
||||
ANSI (16 colors), ANSI Extended (256 colors), and TrueColor (24-bit RGB). Colors
|
||||
will automatically be degraded to the best matching available color in the
|
||||
desired profile:
|
||||
|
||||
`TrueColor` => `ANSI 256 Colors` => `ANSI 16 Colors` => `Ascii`
|
||||
|
||||
```go
|
||||
s := termenv.String("Hello World")
|
||||
|
||||
// Retrieve color profile supported by terminal
|
||||
p := termenv.ColorProfile()
|
||||
s := output.String("Hello World")
|
||||
|
||||
// Supports hex values
|
||||
// Will automatically degrade colors on terminals not supporting RGB
|
||||
s.Foreground(p.Color("#abcdef"))
|
||||
s.Foreground(output.Color("#abcdef"))
|
||||
// but also supports ANSI colors (0-255)
|
||||
s.Background(p.Color("69"))
|
||||
s.Background(output.Color("69"))
|
||||
// ...or the color.Color interface
|
||||
s.Foreground(p.FromColor(color.RGBA{255, 128, 0, 255}))
|
||||
s.Foreground(output.FromColor(color.RGBA{255, 128, 0, 255}))
|
||||
|
||||
// Combine fore- & background colors
|
||||
s.Foreground(p.Color("#ffffff")).Background(p.Color("#0000ff"))
|
||||
s.Foreground(output.Color("#ffffff")).Background(output.Color("#0000ff"))
|
||||
|
||||
// Supports the fmt.Stringer interface
|
||||
fmt.Println(s)
|
||||
|
@ -100,7 +105,7 @@ fmt.Println(s)
|
|||
You can use a chainable syntax to compose your own styles:
|
||||
|
||||
```go
|
||||
s := termenv.String("foobar")
|
||||
s := output.String("foobar")
|
||||
|
||||
// Text styles
|
||||
s.Bold()
|
||||
|
@ -122,9 +127,11 @@ s.Bold().Underline()
|
|||
|
||||
## Template Helpers
|
||||
|
||||
`termenv` provides a set of helper functions to style your Go templates:
|
||||
|
||||
```go
|
||||
// load template helpers
|
||||
f := termenv.TemplateFuncs(termenv.ColorProfile())
|
||||
f := output.TemplateFuncs()
|
||||
tpl := template.New("tpl").Funcs(f)
|
||||
|
||||
// apply bold style in a template
|
||||
|
@ -153,153 +160,170 @@ Other available helper functions are: `Faint`, `Italic`, `CrossOut`,
|
|||
|
||||
```go
|
||||
// Move the cursor to a given position
|
||||
termenv.MoveCursor(row, column)
|
||||
output.MoveCursor(row, column)
|
||||
|
||||
// Save the cursor position
|
||||
termenv.SaveCursorPosition()
|
||||
output.SaveCursorPosition()
|
||||
|
||||
// Restore a saved cursor position
|
||||
termenv.RestoreCursorPosition()
|
||||
output.RestoreCursorPosition()
|
||||
|
||||
// Move the cursor up a given number of lines
|
||||
termenv.CursorUp(n)
|
||||
output.CursorUp(n)
|
||||
|
||||
// Move the cursor down a given number of lines
|
||||
termenv.CursorDown(n)
|
||||
output.CursorDown(n)
|
||||
|
||||
// Move the cursor up a given number of lines
|
||||
termenv.CursorForward(n)
|
||||
output.CursorForward(n)
|
||||
|
||||
// Move the cursor backwards a given number of cells
|
||||
termenv.CursorBack(n)
|
||||
output.CursorBack(n)
|
||||
|
||||
// Move the cursor down a given number of lines and place it at the beginning
|
||||
// of the line
|
||||
termenv.CursorNextLine(n)
|
||||
output.CursorNextLine(n)
|
||||
|
||||
// Move the cursor up a given number of lines and place it at the beginning of
|
||||
// the line
|
||||
termenv.CursorPrevLine(n)
|
||||
output.CursorPrevLine(n)
|
||||
```
|
||||
|
||||
## Screen
|
||||
|
||||
```go
|
||||
// Reset the terminal to its default style, removing any active styles
|
||||
termenv.Reset()
|
||||
output.Reset()
|
||||
|
||||
// RestoreScreen restores a previously saved screen state
|
||||
termenv.RestoreScreen()
|
||||
output.RestoreScreen()
|
||||
|
||||
// SaveScreen saves the screen state
|
||||
termenv.SaveScreen()
|
||||
output.SaveScreen()
|
||||
|
||||
// Switch to the altscreen. The former view can be restored with ExitAltScreen()
|
||||
termenv.AltScreen()
|
||||
output.AltScreen()
|
||||
|
||||
// Exit the altscreen and return to the former terminal view
|
||||
termenv.ExitAltScreen()
|
||||
output.ExitAltScreen()
|
||||
|
||||
// Clear the visible portion of the terminal
|
||||
termenv.ClearScreen()
|
||||
output.ClearScreen()
|
||||
|
||||
// Clear the current line
|
||||
termenv.ClearLine()
|
||||
output.ClearLine()
|
||||
|
||||
// Clear a given number of lines
|
||||
termenv.ClearLines(n)
|
||||
output.ClearLines(n)
|
||||
|
||||
// Set the scrolling region of the terminal
|
||||
termenv.ChangeScrollingRegion(top, bottom)
|
||||
output.ChangeScrollingRegion(top, bottom)
|
||||
|
||||
// Insert the given number of lines at the top of the scrollable region, pushing
|
||||
// lines below down
|
||||
termenv.InsertLines(n)
|
||||
output.InsertLines(n)
|
||||
|
||||
// Delete the given number of lines, pulling any lines in the scrollable region
|
||||
// below up
|
||||
termenv.DeleteLines(n)
|
||||
output.DeleteLines(n)
|
||||
```
|
||||
|
||||
## Session
|
||||
|
||||
```go
|
||||
// SetWindowTitle sets the terminal window title
|
||||
termenv.SetWindowTitle(title)
|
||||
output.SetWindowTitle(title)
|
||||
|
||||
// SetForegroundColor sets the default foreground color
|
||||
termenv.SetForegroundColor(color)
|
||||
output.SetForegroundColor(color)
|
||||
|
||||
// SetBackgroundColor sets the default background color
|
||||
termenv.SetBackgroundColor(color)
|
||||
output.SetBackgroundColor(color)
|
||||
|
||||
// SetCursorColor sets the cursor color
|
||||
termenv.SetCursorColor(color)
|
||||
output.SetCursorColor(color)
|
||||
|
||||
// Hide the cursor
|
||||
termenv.HideCursor()
|
||||
output.HideCursor()
|
||||
|
||||
// Show the cursor
|
||||
termenv.ShowCursor()
|
||||
output.ShowCursor()
|
||||
```
|
||||
|
||||
## Mouse
|
||||
|
||||
```go
|
||||
// Enable X10 mouse mode, only button press events are sent
|
||||
termenv.EnableMousePress()
|
||||
output.EnableMousePress()
|
||||
|
||||
// Disable X10 mouse mode
|
||||
termenv.DisableMousePress()
|
||||
output.DisableMousePress()
|
||||
|
||||
// Enable Mouse Tracking mode
|
||||
termenv.EnableMouse()
|
||||
output.EnableMouse()
|
||||
|
||||
// Disable Mouse Tracking mode
|
||||
termenv.DisableMouse()
|
||||
output.DisableMouse()
|
||||
|
||||
// Enable Hilite Mouse Tracking mode
|
||||
termenv.EnableMouseHilite()
|
||||
output.EnableMouseHilite()
|
||||
|
||||
// Disable Hilite Mouse Tracking mode
|
||||
termenv.DisableMouseHilite()
|
||||
output.DisableMouseHilite()
|
||||
|
||||
// Enable Cell Motion Mouse Tracking mode
|
||||
termenv.EnableMouseCellMotion()
|
||||
output.EnableMouseCellMotion()
|
||||
|
||||
// Disable Cell Motion Mouse Tracking mode
|
||||
termenv.DisableMouseCellMotion()
|
||||
output.DisableMouseCellMotion()
|
||||
|
||||
// Enable All Motion Mouse mode
|
||||
termenv.EnableMouseAllMotion()
|
||||
output.EnableMouseAllMotion()
|
||||
|
||||
// Disable All Motion Mouse mode
|
||||
termenv.DisableMouseAllMotion()
|
||||
output.DisableMouseAllMotion()
|
||||
```
|
||||
|
||||
## Bracketed Paste
|
||||
|
||||
```go
|
||||
// Enables bracketed paste mode
|
||||
termenv.EnableBracketedPaste()
|
||||
|
||||
// Disables bracketed paste mode
|
||||
termenv.DisableBracketedPaste()
|
||||
```
|
||||
|
||||
## Optional Feature Support
|
||||
|
||||
| Terminal | Alt Screen | Query Color Scheme | Query Cursor Position | Set Window Title | Change Cursor Color | Change Default Foreground Setting | Change Default Background Setting |
|
||||
| ---------------- | :--------: | :----------------: | :-------------------: | :--------------: | :-----------------: | :-------------------------------: | :-------------------------------: |
|
||||
| alacritty | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| foot | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| kitty | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Konsole | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
|
||||
| rxvt | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| screen | ✅ | ⛔[^mux] | ✅ | ✅ | ❌ | ❌ | ✅ |
|
||||
| st | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| tmux | ✅ | ⛔[^mux] | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| vte-based[^vte] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| wezterm | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| xterm | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| Linux Console | ✅ | ❌ | ✅ | ⛔ | ❌ | ❌ | ❌ |
|
||||
| Apple Terminal | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
|
||||
| iTerm | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| Windows cmd | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Windows Terminal | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Terminal | Alt Screen | Query Color Scheme | Query Cursor Position | Set Window Title | Change Cursor Color | Change Default Foreground Setting | Change Default Background Setting | Copy (OSC52) | Hyperlinks (OSC8) | Bracketed Paste |
|
||||
| ---------------- | :--------: | :----------------: | :-------------------: | :--------------: | :-----------------: | :-------------------------------: | :-------------------------------: | :----------: | :---------------: | :-------------: |
|
||||
| alacritty | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌[^alacritty] | ✅ |
|
||||
| foot | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| kitty | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Konsole | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌[^konsole] | ✅ | ✅ |
|
||||
| rxvt | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
|
||||
| urxvt | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅[^urxvt] | ❌ | ✅ |
|
||||
| screen | ✅ | ⛔[^mux] | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌[^screen] | ❌ |
|
||||
| st | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
| tmux | ✅ | ⛔[^mux] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌[^tmux] | ✅ |
|
||||
| vte-based[^vte] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌[^vte] | ✅ | ✅ |
|
||||
| wezterm | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| xterm | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ |
|
||||
| Linux Console | ✅ | ❌ | ✅ | ⛔ | ❌ | ❌ | ❌ | ⛔ | ⛔ | ❌ |
|
||||
| Apple Terminal | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅[^apple] | ❌ | ✅ |
|
||||
| iTerm | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||
| Windows cmd | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| Windows Terminal | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
[^vte]: This covers all vte-based terminals, including Gnome Terminal, guake, Pantheon Terminal, Terminator, Tilix, XFCE Terminal.
|
||||
[^vte]: This covers all vte-based terminals, including Gnome Terminal, guake, Pantheon Terminal, Terminator, Tilix, XFCE Terminal. OSC52 is not supported, see [issue#2495](https://gitlab.gnome.org/GNOME/vte/-/issues/2495).
|
||||
[^mux]: Unavailable as multiplexers (like tmux or screen) can be connected to multiple terminals (with different color settings) at the same time.
|
||||
[^urxvt]: Workaround for urxvt not supporting OSC52. See [this](https://unix.stackexchange.com/a/629485) for more information.
|
||||
[^konsole]: OSC52 is not supported, for more info see [bug#372116](https://bugs.kde.org/show_bug.cgi?id=372116).
|
||||
[^apple]: OSC52 works with a [workaround](https://github.com/roy2220/osc52pty).
|
||||
[^tmux]: OSC8 is not supported, for more info see [issue#911](https://github.com/tmux/tmux/issues/911).
|
||||
[^screen]: OSC8 is not supported, for more info see [bug#50952](https://savannah.gnu.org/bugs/index.php?50952).
|
||||
[^alacritty]: OSC8 is not supported, for more info see [issue#922](https://github.com/alacritty/alacritty/issues/922).
|
||||
|
||||
You can help improve this list! Check out [how to](ansi_compat.md) and open an issue or pull request.
|
||||
|
||||
|
@ -316,13 +340,17 @@ terminal applications on Unix support ANSI styling out-of-the-box, on Windows
|
|||
you need to enable ANSI processing in your application first:
|
||||
|
||||
```go
|
||||
mode, err := termenv.EnableWindowsANSIConsole()
|
||||
restoreConsole, err := termenv.EnableVirtualTerminalProcessing(termenv.DefaultOutput())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer termenv.RestoreWindowsConsole(mode)
|
||||
defer restoreConsole()
|
||||
```
|
||||
|
||||
The above code is safe to include on non-Windows systems or when os.Stdout does
|
||||
not refer to a terminal (e.g. in tests).
|
||||
|
||||
|
||||
## Color Chart
|
||||
|
||||
![ANSI color chart](https://github.com/muesli/termenv/raw/master/examples/color-chart/color-chart.png)
|
||||
|
@ -351,8 +379,8 @@ out these projects:
|
|||
|
||||
Got some feedback or suggestions? Please open an issue or drop me a note!
|
||||
|
||||
* [Twitter](https://twitter.com/mueslix)
|
||||
* [The Fediverse](https://mastodon.social/@fribbledom)
|
||||
- [Twitter](https://twitter.com/mueslix)
|
||||
- [The Fediverse](https://mastodon.social/@fribbledom)
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
@ -46,3 +46,12 @@ This command should set the window title to "Test":
|
|||
```bash
|
||||
echo -ne "\033]2;Test\007" && sleep 10
|
||||
```
|
||||
|
||||
## Bracketed paste
|
||||
|
||||
Enter this command, then paste a word from the clipboard. The text
|
||||
displayed on the terminal should contain the codes `200~` and `201~`:
|
||||
|
||||
```bash
|
||||
echo -ne "\033[?2004h" && sleep 10
|
||||
```
|
||||
|
|
|
@ -3,9 +3,7 @@ package termenv
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
|
@ -69,72 +67,6 @@ func ConvertToRGB(c Color) colorful.Color {
|
|||
return ch
|
||||
}
|
||||
|
||||
// Convert transforms a given Color to a Color supported within the Profile.
|
||||
func (p Profile) Convert(c Color) Color {
|
||||
if p == Ascii {
|
||||
return NoColor{}
|
||||
}
|
||||
|
||||
switch v := c.(type) {
|
||||
case ANSIColor:
|
||||
return v
|
||||
|
||||
case ANSI256Color:
|
||||
if p == ANSI {
|
||||
return ansi256ToANSIColor(v)
|
||||
}
|
||||
return v
|
||||
|
||||
case RGBColor:
|
||||
h, err := colorful.Hex(string(v))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if p < TrueColor {
|
||||
ac := hexToANSI256Color(h)
|
||||
if p == ANSI {
|
||||
return ansi256ToANSIColor(ac)
|
||||
}
|
||||
return ac
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Color creates a Color from a string. Valid inputs are hex colors, as well as
|
||||
// ANSI color codes (0-15, 16-255).
|
||||
func (p Profile) Color(s string) Color {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var c Color
|
||||
if strings.HasPrefix(s, "#") {
|
||||
c = RGBColor(s)
|
||||
} else {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if i < 16 {
|
||||
c = ANSIColor(i)
|
||||
} else {
|
||||
c = ANSI256Color(i)
|
||||
}
|
||||
}
|
||||
|
||||
return p.Convert(c)
|
||||
}
|
||||
|
||||
// FromColor creates a Color from a color.Color.
|
||||
func (p Profile) FromColor(c color.Color) Color {
|
||||
col, _ := colorful.MakeColor(c)
|
||||
return p.Color(col.Hex())
|
||||
}
|
||||
|
||||
// Sequence returns the ANSI Sequence for the color.
|
||||
func (c NoColor) Sequence(bg bool) string {
|
||||
return ""
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package termenv
|
||||
|
||||
import (
|
||||
"github.com/aymanbagabas/go-osc52"
|
||||
)
|
||||
|
||||
// Copy copies text to clipboard using OSC 52 escape sequence.
|
||||
func (o Output) Copy(str string) {
|
||||
out := osc52.NewOutput(o.tty, o.environ.Environ())
|
||||
out.Copy(str)
|
||||
}
|
||||
|
||||
// Copy copies text to clipboard using OSC 52 escape sequence.
|
||||
func Copy(str string) {
|
||||
output.Copy(str)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package termenv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Hyperlink creates a hyperlink using OSC8.
|
||||
func Hyperlink(link, name string) string {
|
||||
return output.Hyperlink(link, name)
|
||||
}
|
||||
|
||||
// Hyperlink creates a hyperlink using OSC8.
|
||||
func (o *Output) Hyperlink(link, name string) string {
|
||||
return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", link, name)
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
package termenv
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
// output is the default global output.
|
||||
output = NewOutput(os.Stdout)
|
||||
)
|
||||
|
||||
// File represents a file descriptor.
|
||||
type File interface {
|
||||
io.ReadWriter
|
||||
Fd() uintptr
|
||||
}
|
||||
|
||||
// Output is a terminal output.
|
||||
type Output struct {
|
||||
Profile
|
||||
tty io.Writer
|
||||
environ Environ
|
||||
|
||||
cache bool
|
||||
fgSync *sync.Once
|
||||
fgColor Color
|
||||
bgSync *sync.Once
|
||||
bgColor Color
|
||||
}
|
||||
|
||||
// Environ is an interface for getting environment variables.
|
||||
type Environ interface {
|
||||
Environ() []string
|
||||
Getenv(string) string
|
||||
}
|
||||
|
||||
type osEnviron struct{}
|
||||
|
||||
func (oe *osEnviron) Environ() []string {
|
||||
return os.Environ()
|
||||
}
|
||||
|
||||
func (oe *osEnviron) Getenv(key string) string {
|
||||
return os.Getenv(key)
|
||||
}
|
||||
|
||||
// DefaultOutput returns the default global output.
|
||||
func DefaultOutput() *Output {
|
||||
return output
|
||||
}
|
||||
|
||||
// NewOutput returns a new Output for the given file descriptor.
|
||||
func NewOutput(tty io.Writer, opts ...func(*Output)) *Output {
|
||||
o := &Output{
|
||||
tty: tty,
|
||||
environ: &osEnviron{},
|
||||
Profile: -1,
|
||||
fgSync: &sync.Once{},
|
||||
fgColor: NoColor{},
|
||||
bgSync: &sync.Once{},
|
||||
bgColor: NoColor{},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
if o.Profile < 0 {
|
||||
o.Profile = o.EnvColorProfile()
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
// WithEnvironment returns a new Output for the given environment.
|
||||
func WithEnvironment(environ Environ) func(*Output) {
|
||||
return func(o *Output) {
|
||||
o.environ = environ
|
||||
}
|
||||
}
|
||||
|
||||
// WithProfile returns a new Output for the given profile.
|
||||
func WithProfile(profile Profile) func(*Output) {
|
||||
return func(o *Output) {
|
||||
o.Profile = profile
|
||||
}
|
||||
}
|
||||
|
||||
// WithColorCache returns a new Output with fore- and background color values
|
||||
// pre-fetched and cached.
|
||||
func WithColorCache(v bool) func(*Output) {
|
||||
return func(o *Output) {
|
||||
o.cache = v
|
||||
|
||||
// cache the values now
|
||||
_ = o.ForegroundColor()
|
||||
_ = o.BackgroundColor()
|
||||
}
|
||||
}
|
||||
|
||||
// ForegroundColor returns the terminal's default foreground color.
|
||||
func (o *Output) ForegroundColor() Color {
|
||||
f := func() {
|
||||
if !o.isTTY() {
|
||||
return
|
||||
}
|
||||
|
||||
o.fgColor = o.foregroundColor()
|
||||
}
|
||||
|
||||
if o.cache {
|
||||
o.fgSync.Do(f)
|
||||
} else {
|
||||
f()
|
||||
}
|
||||
|
||||
return o.fgColor
|
||||
}
|
||||
|
||||
// BackgroundColor returns the terminal's default background color.
|
||||
func (o *Output) BackgroundColor() Color {
|
||||
f := func() {
|
||||
if !o.isTTY() {
|
||||
return
|
||||
}
|
||||
|
||||
o.bgColor = o.backgroundColor()
|
||||
}
|
||||
|
||||
if o.cache {
|
||||
o.bgSync.Do(f)
|
||||
} else {
|
||||
f()
|
||||
}
|
||||
|
||||
return o.bgColor
|
||||
}
|
||||
|
||||
// HasDarkBackground returns whether terminal uses a dark-ish background.
|
||||
func (o *Output) HasDarkBackground() bool {
|
||||
c := ConvertToRGB(o.BackgroundColor())
|
||||
_, _, l := c.Hsl()
|
||||
return l < 0.5
|
||||
}
|
||||
|
||||
// TTY returns the terminal's file descriptor. This may be nil if the output is
|
||||
// not a terminal.
|
||||
func (o Output) TTY() File {
|
||||
if f, ok := o.tty.(File); ok {
|
||||
return f
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o Output) Write(p []byte) (int, error) {
|
||||
return o.tty.Write(p)
|
||||
}
|
||||
|
||||
// WriteString writes the given string to the output.
|
||||
func (o Output) WriteString(s string) (int, error) {
|
||||
return o.Write([]byte(s))
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package termenv
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
)
|
||||
|
||||
// Profile is a color profile: Ascii, ANSI, ANSI256, or TrueColor.
|
||||
type Profile int
|
||||
|
||||
const (
|
||||
// TrueColor, 24-bit color profile
|
||||
TrueColor = Profile(iota)
|
||||
// ANSI256, 8-bit color profile
|
||||
ANSI256
|
||||
// ANSI, 4-bit color profile
|
||||
ANSI
|
||||
// Ascii, uncolored profile
|
||||
Ascii //nolint:revive
|
||||
)
|
||||
|
||||
// String returns a new Style.
|
||||
func (p Profile) String(s ...string) Style {
|
||||
return Style{
|
||||
profile: p,
|
||||
string: strings.Join(s, " "),
|
||||
}
|
||||
}
|
||||
|
||||
// Convert transforms a given Color to a Color supported within the Profile.
|
||||
func (p Profile) Convert(c Color) Color {
|
||||
if p == Ascii {
|
||||
return NoColor{}
|
||||
}
|
||||
|
||||
switch v := c.(type) {
|
||||
case ANSIColor:
|
||||
return v
|
||||
|
||||
case ANSI256Color:
|
||||
if p == ANSI {
|
||||
return ansi256ToANSIColor(v)
|
||||
}
|
||||
return v
|
||||
|
||||
case RGBColor:
|
||||
h, err := colorful.Hex(string(v))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if p != TrueColor {
|
||||
ac := hexToANSI256Color(h)
|
||||
if p == ANSI {
|
||||
return ansi256ToANSIColor(ac)
|
||||
}
|
||||
return ac
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Color creates a Color from a string. Valid inputs are hex colors, as well as
|
||||
// ANSI color codes (0-15, 16-255).
|
||||
func (p Profile) Color(s string) Color {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var c Color
|
||||
if strings.HasPrefix(s, "#") {
|
||||
c = RGBColor(s)
|
||||
} else {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if i < 16 {
|
||||
c = ANSIColor(i)
|
||||
} else {
|
||||
c = ANSI256Color(i)
|
||||
}
|
||||
}
|
||||
|
||||
return p.Convert(c)
|
||||
}
|
||||
|
||||
// FromColor creates a Color from a color.Color.
|
||||
func (p Profile) FromColor(c color.Color) Color {
|
||||
col, _ := colorful.MakeColor(c)
|
||||
return p.Color(col.Hex())
|
||||
}
|
|
@ -49,6 +49,13 @@ const (
|
|||
AltScreenSeq = "?1049h"
|
||||
ExitAltScreenSeq = "?1049l"
|
||||
|
||||
// Bracketed paste.
|
||||
// https://en.wikipedia.org/wiki/Bracketed-paste
|
||||
EnableBracketedPasteSeq = "?2004h"
|
||||
DisableBracketedPasteSeq = "?2004l"
|
||||
StartBracketedPasteSeq = "200~"
|
||||
EndBracketedPasteSeq = "201~"
|
||||
|
||||
// Session.
|
||||
SetWindowTitleSeq = "2;%s\007"
|
||||
SetForegroundColorSeq = "10;%s\007"
|
||||
|
@ -59,200 +66,498 @@ const (
|
|||
)
|
||||
|
||||
// Reset the terminal to its default style, removing any active styles.
|
||||
func Reset() {
|
||||
fmt.Print(CSI + ResetSeq + "m")
|
||||
func (o Output) Reset() {
|
||||
fmt.Fprint(o.tty, CSI+ResetSeq+"m")
|
||||
}
|
||||
|
||||
// SetForegroundColor sets the default foreground color.
|
||||
func SetForegroundColor(color Color) {
|
||||
fmt.Printf(OSC+SetForegroundColorSeq, color)
|
||||
func (o Output) SetForegroundColor(color Color) {
|
||||
fmt.Fprintf(o.tty, OSC+SetForegroundColorSeq, color)
|
||||
}
|
||||
|
||||
// SetBackgroundColor sets the default background color.
|
||||
func SetBackgroundColor(color Color) {
|
||||
fmt.Printf(OSC+SetBackgroundColorSeq, color)
|
||||
func (o Output) SetBackgroundColor(color Color) {
|
||||
fmt.Fprintf(o.tty, OSC+SetBackgroundColorSeq, color)
|
||||
}
|
||||
|
||||
// SetCursorColor sets the cursor color.
|
||||
func SetCursorColor(color Color) {
|
||||
fmt.Printf(OSC+SetCursorColorSeq, color)
|
||||
func (o Output) SetCursorColor(color Color) {
|
||||
fmt.Fprintf(o.tty, OSC+SetCursorColorSeq, color)
|
||||
}
|
||||
|
||||
// RestoreScreen restores a previously saved screen state.
|
||||
func RestoreScreen() {
|
||||
fmt.Print(CSI + RestoreScreenSeq)
|
||||
func (o Output) RestoreScreen() {
|
||||
fmt.Fprint(o.tty, CSI+RestoreScreenSeq)
|
||||
}
|
||||
|
||||
// SaveScreen saves the screen state.
|
||||
func SaveScreen() {
|
||||
fmt.Print(CSI + SaveScreenSeq)
|
||||
func (o Output) SaveScreen() {
|
||||
fmt.Fprint(o.tty, CSI+SaveScreenSeq)
|
||||
}
|
||||
|
||||
// AltScreen switches to the alternate screen buffer. The former view can be
|
||||
// restored with ExitAltScreen().
|
||||
func AltScreen() {
|
||||
fmt.Print(CSI + AltScreenSeq)
|
||||
func (o Output) AltScreen() {
|
||||
fmt.Fprint(o.tty, CSI+AltScreenSeq)
|
||||
}
|
||||
|
||||
// ExitAltScreen exits the alternate screen buffer and returns to the former
|
||||
// terminal view.
|
||||
func ExitAltScreen() {
|
||||
fmt.Print(CSI + ExitAltScreenSeq)
|
||||
func (o Output) ExitAltScreen() {
|
||||
fmt.Fprint(o.tty, CSI+ExitAltScreenSeq)
|
||||
}
|
||||
|
||||
// ClearScreen clears the visible portion of the terminal.
|
||||
func ClearScreen() {
|
||||
fmt.Printf(CSI+EraseDisplaySeq, 2)
|
||||
MoveCursor(1, 1)
|
||||
func (o Output) ClearScreen() {
|
||||
fmt.Fprintf(o.tty, CSI+EraseDisplaySeq, 2)
|
||||
o.MoveCursor(1, 1)
|
||||
}
|
||||
|
||||
// MoveCursor moves the cursor to a given position.
|
||||
func MoveCursor(row int, column int) {
|
||||
fmt.Printf(CSI+CursorPositionSeq, row, column)
|
||||
func (o Output) MoveCursor(row int, column int) {
|
||||
fmt.Fprintf(o.tty, CSI+CursorPositionSeq, row, column)
|
||||
}
|
||||
|
||||
// HideCursor hides the cursor.
|
||||
func HideCursor() {
|
||||
fmt.Printf(CSI + HideCursorSeq)
|
||||
func (o Output) HideCursor() {
|
||||
fmt.Fprint(o.tty, CSI+HideCursorSeq)
|
||||
}
|
||||
|
||||
// ShowCursor shows the cursor.
|
||||
func ShowCursor() {
|
||||
fmt.Printf(CSI + ShowCursorSeq)
|
||||
func (o Output) ShowCursor() {
|
||||
fmt.Fprint(o.tty, CSI+ShowCursorSeq)
|
||||
}
|
||||
|
||||
// SaveCursorPosition saves the cursor position.
|
||||
func SaveCursorPosition() {
|
||||
fmt.Print(CSI + SaveCursorPositionSeq)
|
||||
func (o Output) SaveCursorPosition() {
|
||||
fmt.Fprint(o.tty, CSI+SaveCursorPositionSeq)
|
||||
}
|
||||
|
||||
// RestoreCursorPosition restores a saved cursor position.
|
||||
func RestoreCursorPosition() {
|
||||
fmt.Print(CSI + RestoreCursorPositionSeq)
|
||||
func (o Output) RestoreCursorPosition() {
|
||||
fmt.Fprint(o.tty, CSI+RestoreCursorPositionSeq)
|
||||
}
|
||||
|
||||
// CursorUp moves the cursor up a given number of lines.
|
||||
func CursorUp(n int) {
|
||||
fmt.Printf(CSI+CursorUpSeq, n)
|
||||
func (o Output) CursorUp(n int) {
|
||||
fmt.Fprintf(o.tty, CSI+CursorUpSeq, n)
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down a given number of lines.
|
||||
func CursorDown(n int) {
|
||||
fmt.Printf(CSI+CursorDownSeq, n)
|
||||
func (o Output) CursorDown(n int) {
|
||||
fmt.Fprintf(o.tty, CSI+CursorDownSeq, n)
|
||||
}
|
||||
|
||||
// CursorForward moves the cursor up a given number of lines.
|
||||
func CursorForward(n int) {
|
||||
fmt.Printf(CSI+CursorForwardSeq, n)
|
||||
func (o Output) CursorForward(n int) {
|
||||
fmt.Fprintf(o.tty, CSI+CursorForwardSeq, n)
|
||||
}
|
||||
|
||||
// CursorBack moves the cursor backwards a given number of cells.
|
||||
func CursorBack(n int) {
|
||||
fmt.Printf(CSI+CursorBackSeq, n)
|
||||
func (o Output) CursorBack(n int) {
|
||||
fmt.Fprintf(o.tty, CSI+CursorBackSeq, n)
|
||||
}
|
||||
|
||||
// CursorNextLine moves the cursor down a given number of lines and places it at
|
||||
// the beginning of the line.
|
||||
func CursorNextLine(n int) {
|
||||
fmt.Printf(CSI+CursorNextLineSeq, n)
|
||||
func (o Output) CursorNextLine(n int) {
|
||||
fmt.Fprintf(o.tty, CSI+CursorNextLineSeq, n)
|
||||
}
|
||||
|
||||
// CursorPrevLine moves the cursor up a given number of lines and places it at
|
||||
// the beginning of the line.
|
||||
func CursorPrevLine(n int) {
|
||||
fmt.Printf(CSI+CursorPreviousLineSeq, n)
|
||||
func (o Output) CursorPrevLine(n int) {
|
||||
fmt.Fprintf(o.tty, CSI+CursorPreviousLineSeq, n)
|
||||
}
|
||||
|
||||
// ClearLine clears the current line.
|
||||
func ClearLine() {
|
||||
fmt.Print(CSI + EraseEntireLineSeq)
|
||||
func (o Output) ClearLine() {
|
||||
fmt.Fprint(o.tty, CSI+EraseEntireLineSeq)
|
||||
}
|
||||
|
||||
// ClearLineLeft clears the line to the left of the cursor.
|
||||
func ClearLineLeft() {
|
||||
fmt.Print(CSI + EraseLineLeftSeq)
|
||||
func (o Output) ClearLineLeft() {
|
||||
fmt.Fprint(o.tty, CSI+EraseLineLeftSeq)
|
||||
}
|
||||
|
||||
// ClearLineRight clears the line to the right of the cursor.
|
||||
func ClearLineRight() {
|
||||
fmt.Print(CSI + EraseLineRightSeq)
|
||||
func (o Output) ClearLineRight() {
|
||||
fmt.Fprint(o.tty, CSI+EraseLineRightSeq)
|
||||
}
|
||||
|
||||
// ClearLines clears a given number of lines.
|
||||
func ClearLines(n int) {
|
||||
func (o Output) ClearLines(n int) {
|
||||
clearLine := fmt.Sprintf(CSI+EraseLineSeq, 2)
|
||||
cursorUp := fmt.Sprintf(CSI+CursorUpSeq, 1)
|
||||
fmt.Print(clearLine + strings.Repeat(cursorUp+clearLine, n))
|
||||
fmt.Fprint(o.tty, clearLine+strings.Repeat(cursorUp+clearLine, n))
|
||||
}
|
||||
|
||||
// ChangeScrollingRegion sets the scrolling region of the terminal.
|
||||
func ChangeScrollingRegion(top, bottom int) {
|
||||
fmt.Printf(CSI+ChangeScrollingRegionSeq, top, bottom)
|
||||
func (o Output) ChangeScrollingRegion(top, bottom int) {
|
||||
fmt.Fprintf(o.tty, CSI+ChangeScrollingRegionSeq, top, bottom)
|
||||
}
|
||||
|
||||
// InsertLines inserts the given number of lines at the top of the scrollable
|
||||
// region, pushing lines below down.
|
||||
func InsertLines(n int) {
|
||||
fmt.Printf(CSI+InsertLineSeq, n)
|
||||
func (o Output) InsertLines(n int) {
|
||||
fmt.Fprintf(o.tty, CSI+InsertLineSeq, n)
|
||||
}
|
||||
|
||||
// DeleteLines deletes the given number of lines, pulling any lines in
|
||||
// the scrollable region below up.
|
||||
func DeleteLines(n int) {
|
||||
fmt.Printf(CSI+DeleteLineSeq, n)
|
||||
func (o Output) DeleteLines(n int) {
|
||||
fmt.Fprintf(o.tty, CSI+DeleteLineSeq, n)
|
||||
}
|
||||
|
||||
// EnableMousePress enables X10 mouse mode. Button press events are sent only.
|
||||
func EnableMousePress() {
|
||||
fmt.Print(CSI + EnableMousePressSeq)
|
||||
func (o Output) EnableMousePress() {
|
||||
fmt.Fprint(o.tty, CSI+EnableMousePressSeq)
|
||||
}
|
||||
|
||||
// DisableMousePress disables X10 mouse mode.
|
||||
func DisableMousePress() {
|
||||
fmt.Print(CSI + DisableMousePressSeq)
|
||||
func (o Output) DisableMousePress() {
|
||||
fmt.Fprint(o.tty, CSI+DisableMousePressSeq)
|
||||
}
|
||||
|
||||
// EnableMouse enables Mouse Tracking mode.
|
||||
func EnableMouse() {
|
||||
fmt.Print(CSI + EnableMouseSeq)
|
||||
func (o Output) EnableMouse() {
|
||||
fmt.Fprint(o.tty, CSI+EnableMouseSeq)
|
||||
}
|
||||
|
||||
// DisableMouse disables Mouse Tracking mode.
|
||||
func DisableMouse() {
|
||||
fmt.Print(CSI + DisableMouseSeq)
|
||||
func (o Output) DisableMouse() {
|
||||
fmt.Fprint(o.tty, CSI+DisableMouseSeq)
|
||||
}
|
||||
|
||||
// EnableMouseHilite enables Hilite Mouse Tracking mode.
|
||||
func EnableMouseHilite() {
|
||||
fmt.Print(CSI + EnableMouseHiliteSeq)
|
||||
func (o Output) EnableMouseHilite() {
|
||||
fmt.Fprint(o.tty, CSI+EnableMouseHiliteSeq)
|
||||
}
|
||||
|
||||
// DisableMouseHilite disables Hilite Mouse Tracking mode.
|
||||
func DisableMouseHilite() {
|
||||
fmt.Print(CSI + DisableMouseHiliteSeq)
|
||||
func (o Output) DisableMouseHilite() {
|
||||
fmt.Fprint(o.tty, CSI+DisableMouseHiliteSeq)
|
||||
}
|
||||
|
||||
// EnableMouseCellMotion enables Cell Motion Mouse Tracking mode.
|
||||
func EnableMouseCellMotion() {
|
||||
fmt.Print(CSI + EnableMouseCellMotionSeq)
|
||||
func (o Output) EnableMouseCellMotion() {
|
||||
fmt.Fprint(o.tty, CSI+EnableMouseCellMotionSeq)
|
||||
}
|
||||
|
||||
// DisableMouseCellMotion disables Cell Motion Mouse Tracking mode.
|
||||
func DisableMouseCellMotion() {
|
||||
fmt.Print(CSI + DisableMouseCellMotionSeq)
|
||||
func (o Output) DisableMouseCellMotion() {
|
||||
fmt.Fprint(o.tty, CSI+DisableMouseCellMotionSeq)
|
||||
}
|
||||
|
||||
// EnableMouseAllMotion enables All Motion Mouse mode.
|
||||
func EnableMouseAllMotion() {
|
||||
fmt.Print(CSI + EnableMouseAllMotionSeq)
|
||||
func (o Output) EnableMouseAllMotion() {
|
||||
fmt.Fprint(o.tty, CSI+EnableMouseAllMotionSeq)
|
||||
}
|
||||
|
||||
// DisableMouseAllMotion disables All Motion Mouse mode.
|
||||
func DisableMouseAllMotion() {
|
||||
fmt.Print(CSI + DisableMouseAllMotionSeq)
|
||||
func (o Output) DisableMouseAllMotion() {
|
||||
fmt.Fprint(o.tty, CSI+DisableMouseAllMotionSeq)
|
||||
}
|
||||
|
||||
// SetWindowTitle sets the terminal window title.
|
||||
func SetWindowTitle(title string) {
|
||||
fmt.Printf(OSC+SetWindowTitleSeq, title)
|
||||
func (o Output) SetWindowTitle(title string) {
|
||||
fmt.Fprintf(o.tty, OSC+SetWindowTitleSeq, title)
|
||||
}
|
||||
|
||||
// EnableBracketedPaste enables bracketed paste.
|
||||
func (o Output) EnableBracketedPaste() {
|
||||
fmt.Fprintf(o.tty, CSI+EnableBracketedPasteSeq)
|
||||
}
|
||||
|
||||
// DisableBracketedPaste disables bracketed paste.
|
||||
func (o Output) DisableBracketedPaste() {
|
||||
fmt.Fprintf(o.tty, CSI+DisableBracketedPasteSeq)
|
||||
}
|
||||
|
||||
// Legacy functions.
|
||||
|
||||
// Reset the terminal to its default style, removing any active styles.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func Reset() {
|
||||
output.Reset()
|
||||
}
|
||||
|
||||
// SetForegroundColor sets the default foreground color.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func SetForegroundColor(color Color) {
|
||||
output.SetForegroundColor(color)
|
||||
}
|
||||
|
||||
// SetBackgroundColor sets the default background color.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func SetBackgroundColor(color Color) {
|
||||
output.SetBackgroundColor(color)
|
||||
}
|
||||
|
||||
// SetCursorColor sets the cursor color.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func SetCursorColor(color Color) {
|
||||
output.SetCursorColor(color)
|
||||
}
|
||||
|
||||
// RestoreScreen restores a previously saved screen state.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func RestoreScreen() {
|
||||
output.RestoreScreen()
|
||||
}
|
||||
|
||||
// SaveScreen saves the screen state.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func SaveScreen() {
|
||||
output.SaveScreen()
|
||||
}
|
||||
|
||||
// AltScreen switches to the alternate screen buffer. The former view can be
|
||||
// restored with ExitAltScreen().
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func AltScreen() {
|
||||
output.AltScreen()
|
||||
}
|
||||
|
||||
// ExitAltScreen exits the alternate screen buffer and returns to the former
|
||||
// terminal view.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func ExitAltScreen() {
|
||||
output.ExitAltScreen()
|
||||
}
|
||||
|
||||
// ClearScreen clears the visible portion of the terminal.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func ClearScreen() {
|
||||
output.ClearScreen()
|
||||
}
|
||||
|
||||
// MoveCursor moves the cursor to a given position.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func MoveCursor(row int, column int) {
|
||||
output.MoveCursor(row, column)
|
||||
}
|
||||
|
||||
// HideCursor hides the cursor.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func HideCursor() {
|
||||
output.HideCursor()
|
||||
}
|
||||
|
||||
// ShowCursor shows the cursor.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func ShowCursor() {
|
||||
output.ShowCursor()
|
||||
}
|
||||
|
||||
// SaveCursorPosition saves the cursor position.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func SaveCursorPosition() {
|
||||
output.SaveCursorPosition()
|
||||
}
|
||||
|
||||
// RestoreCursorPosition restores a saved cursor position.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func RestoreCursorPosition() {
|
||||
output.RestoreCursorPosition()
|
||||
}
|
||||
|
||||
// CursorUp moves the cursor up a given number of lines.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func CursorUp(n int) {
|
||||
output.CursorUp(n)
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down a given number of lines.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func CursorDown(n int) {
|
||||
output.CursorDown(n)
|
||||
}
|
||||
|
||||
// CursorForward moves the cursor up a given number of lines.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func CursorForward(n int) {
|
||||
output.CursorForward(n)
|
||||
}
|
||||
|
||||
// CursorBack moves the cursor backwards a given number of cells.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func CursorBack(n int) {
|
||||
output.CursorBack(n)
|
||||
}
|
||||
|
||||
// CursorNextLine moves the cursor down a given number of lines and places it at
|
||||
// the beginning of the line.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func CursorNextLine(n int) {
|
||||
output.CursorNextLine(n)
|
||||
}
|
||||
|
||||
// CursorPrevLine moves the cursor up a given number of lines and places it at
|
||||
// the beginning of the line.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func CursorPrevLine(n int) {
|
||||
output.CursorPrevLine(n)
|
||||
}
|
||||
|
||||
// ClearLine clears the current line.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func ClearLine() {
|
||||
output.ClearLine()
|
||||
}
|
||||
|
||||
// ClearLineLeft clears the line to the left of the cursor.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func ClearLineLeft() {
|
||||
output.ClearLineLeft()
|
||||
}
|
||||
|
||||
// ClearLineRight clears the line to the right of the cursor.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func ClearLineRight() {
|
||||
output.ClearLineRight()
|
||||
}
|
||||
|
||||
// ClearLines clears a given number of lines.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func ClearLines(n int) {
|
||||
output.ClearLines(n)
|
||||
}
|
||||
|
||||
// ChangeScrollingRegion sets the scrolling region of the terminal.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func ChangeScrollingRegion(top, bottom int) {
|
||||
output.ChangeScrollingRegion(top, bottom)
|
||||
}
|
||||
|
||||
// InsertLines inserts the given number of lines at the top of the scrollable
|
||||
// region, pushing lines below down.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func InsertLines(n int) {
|
||||
output.InsertLines(n)
|
||||
}
|
||||
|
||||
// DeleteLines deletes the given number of lines, pulling any lines in
|
||||
// the scrollable region below up.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func DeleteLines(n int) {
|
||||
output.DeleteLines(n)
|
||||
}
|
||||
|
||||
// EnableMousePress enables X10 mouse mode. Button press events are sent only.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func EnableMousePress() {
|
||||
output.EnableMousePress()
|
||||
}
|
||||
|
||||
// DisableMousePress disables X10 mouse mode.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func DisableMousePress() {
|
||||
output.DisableMousePress()
|
||||
}
|
||||
|
||||
// EnableMouse enables Mouse Tracking mode.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func EnableMouse() {
|
||||
output.EnableMouse()
|
||||
}
|
||||
|
||||
// DisableMouse disables Mouse Tracking mode.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func DisableMouse() {
|
||||
output.DisableMouse()
|
||||
}
|
||||
|
||||
// EnableMouseHilite enables Hilite Mouse Tracking mode.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func EnableMouseHilite() {
|
||||
output.EnableMouseHilite()
|
||||
}
|
||||
|
||||
// DisableMouseHilite disables Hilite Mouse Tracking mode.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func DisableMouseHilite() {
|
||||
output.DisableMouseHilite()
|
||||
}
|
||||
|
||||
// EnableMouseCellMotion enables Cell Motion Mouse Tracking mode.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func EnableMouseCellMotion() {
|
||||
output.EnableMouseCellMotion()
|
||||
}
|
||||
|
||||
// DisableMouseCellMotion disables Cell Motion Mouse Tracking mode.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func DisableMouseCellMotion() {
|
||||
output.DisableMouseCellMotion()
|
||||
}
|
||||
|
||||
// EnableMouseAllMotion enables All Motion Mouse mode.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func EnableMouseAllMotion() {
|
||||
output.EnableMouseAllMotion()
|
||||
}
|
||||
|
||||
// DisableMouseAllMotion disables All Motion Mouse mode.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func DisableMouseAllMotion() {
|
||||
output.DisableMouseAllMotion()
|
||||
}
|
||||
|
||||
// SetWindowTitle sets the terminal window title.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func SetWindowTitle(title string) {
|
||||
output.SetWindowTitle(title)
|
||||
}
|
||||
|
||||
// EnableBracketedPaste enables bracketed paste.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func EnableBracketedPaste() {
|
||||
output.EnableBracketedPaste()
|
||||
}
|
||||
|
||||
// DisableBracketedPaste disables bracketed paste.
|
||||
//
|
||||
// Deprecated: please use termenv.Output instead.
|
||||
func DisableBracketedPaste() {
|
||||
output.DisableBracketedPaste()
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ const (
|
|||
|
||||
// Style is a string that various rendering styles can be applied to.
|
||||
type Style struct {
|
||||
profile Profile
|
||||
string
|
||||
styles []string
|
||||
}
|
||||
|
@ -29,7 +30,8 @@ type Style struct {
|
|||
// String returns a new Style.
|
||||
func String(s ...string) Style {
|
||||
return Style{
|
||||
string: strings.Join(s, " "),
|
||||
profile: ANSI,
|
||||
string: strings.Join(s, " "),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,6 +41,9 @@ func (t Style) String() string {
|
|||
|
||||
// Styled renders s with all applied styles.
|
||||
func (t Style) Styled(s string) string {
|
||||
if t.profile == Ascii {
|
||||
return s
|
||||
}
|
||||
if len(t.styles) == 0 {
|
||||
return s
|
||||
}
|
||||
|
|
|
@ -4,11 +4,20 @@ import (
|
|||
"text/template"
|
||||
)
|
||||
|
||||
// TemplateFuncs returns template helpers for the given output.
|
||||
func (o Output) TemplateFuncs() template.FuncMap {
|
||||
return TemplateFuncs(o.Profile)
|
||||
}
|
||||
|
||||
// TemplateFuncs contains a few useful template helpers.
|
||||
func TemplateFuncs(p Profile) template.FuncMap {
|
||||
if p == Ascii {
|
||||
return noopTemplateFuncs
|
||||
}
|
||||
|
||||
return template.FuncMap{
|
||||
"Color": func(values ...interface{}) string {
|
||||
s := String(values[len(values)-1].(string))
|
||||
s := p.String(values[len(values)-1].(string))
|
||||
switch len(values) {
|
||||
case 2:
|
||||
s = s.Foreground(p.Color(values[0].(string)))
|
||||
|
@ -21,7 +30,7 @@ func TemplateFuncs(p Profile) template.FuncMap {
|
|||
return s.String()
|
||||
},
|
||||
"Foreground": func(values ...interface{}) string {
|
||||
s := String(values[len(values)-1].(string))
|
||||
s := p.String(values[len(values)-1].(string))
|
||||
if len(values) == 2 {
|
||||
s = s.Foreground(p.Color(values[0].(string)))
|
||||
}
|
||||
|
@ -29,27 +38,49 @@ func TemplateFuncs(p Profile) template.FuncMap {
|
|||
return s.String()
|
||||
},
|
||||
"Background": func(values ...interface{}) string {
|
||||
s := String(values[len(values)-1].(string))
|
||||
s := p.String(values[len(values)-1].(string))
|
||||
if len(values) == 2 {
|
||||
s = s.Background(p.Color(values[0].(string)))
|
||||
}
|
||||
|
||||
return s.String()
|
||||
},
|
||||
"Bold": styleFunc(Style.Bold),
|
||||
"Faint": styleFunc(Style.Faint),
|
||||
"Italic": styleFunc(Style.Italic),
|
||||
"Underline": styleFunc(Style.Underline),
|
||||
"Overline": styleFunc(Style.Overline),
|
||||
"Blink": styleFunc(Style.Blink),
|
||||
"Reverse": styleFunc(Style.Reverse),
|
||||
"CrossOut": styleFunc(Style.CrossOut),
|
||||
"Bold": styleFunc(p, Style.Bold),
|
||||
"Faint": styleFunc(p, Style.Faint),
|
||||
"Italic": styleFunc(p, Style.Italic),
|
||||
"Underline": styleFunc(p, Style.Underline),
|
||||
"Overline": styleFunc(p, Style.Overline),
|
||||
"Blink": styleFunc(p, Style.Blink),
|
||||
"Reverse": styleFunc(p, Style.Reverse),
|
||||
"CrossOut": styleFunc(p, Style.CrossOut),
|
||||
}
|
||||
}
|
||||
|
||||
func styleFunc(f func(Style) Style) func(...interface{}) string {
|
||||
func styleFunc(p Profile, f func(Style) Style) func(...interface{}) string {
|
||||
return func(values ...interface{}) string {
|
||||
s := String(values[0].(string))
|
||||
s := p.String(values[0].(string))
|
||||
return f(s).String()
|
||||
}
|
||||
}
|
||||
|
||||
var noopTemplateFuncs = template.FuncMap{
|
||||
"Color": noColorFunc,
|
||||
"Foreground": noColorFunc,
|
||||
"Background": noColorFunc,
|
||||
"Bold": noStyleFunc,
|
||||
"Faint": noStyleFunc,
|
||||
"Italic": noStyleFunc,
|
||||
"Underline": noStyleFunc,
|
||||
"Overline": noStyleFunc,
|
||||
"Blink": noStyleFunc,
|
||||
"Reverse": noStyleFunc,
|
||||
"CrossOut": noStyleFunc,
|
||||
}
|
||||
|
||||
func noColorFunc(values ...interface{}) string {
|
||||
return values[len(values)-1].(string)
|
||||
}
|
||||
|
||||
func noStyleFunc(values ...interface{}) string {
|
||||
return values[0].(string)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package termenv
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
@ -12,66 +11,52 @@ var (
|
|||
ErrStatusReport = errors.New("unable to retrieve status report")
|
||||
)
|
||||
|
||||
// Profile is a color profile: Ascii, ANSI, ANSI256, or TrueColor.
|
||||
type Profile int
|
||||
|
||||
const (
|
||||
// Control Sequence Introducer
|
||||
CSI = "\x1b["
|
||||
// Operating System Command
|
||||
OSC = "\x1b]"
|
||||
|
||||
// Ascii, uncolored profile.
|
||||
Ascii = Profile(iota) //nolint:revive
|
||||
// ANSI, 4-bit color profile
|
||||
ANSI
|
||||
// ANSI256, 8-bit color profile
|
||||
ANSI256
|
||||
// TrueColor, 24-bit color profile
|
||||
TrueColor
|
||||
)
|
||||
|
||||
func isTTY(fd uintptr) bool {
|
||||
if len(os.Getenv("CI")) > 0 {
|
||||
func (o *Output) isTTY() bool {
|
||||
if len(o.environ.Getenv("CI")) > 0 {
|
||||
return false
|
||||
}
|
||||
if o.TTY() == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return isatty.IsTerminal(fd)
|
||||
return isatty.IsTerminal(o.TTY().Fd())
|
||||
}
|
||||
|
||||
// ColorProfile returns the supported color profile:
|
||||
// Ascii, ANSI, ANSI256, or TrueColor.
|
||||
func ColorProfile() Profile {
|
||||
if !isTTY(os.Stdout.Fd()) {
|
||||
return Ascii
|
||||
}
|
||||
|
||||
return colorProfile()
|
||||
return output.ColorProfile()
|
||||
}
|
||||
|
||||
// ForegroundColor returns the terminal's default foreground color.
|
||||
func ForegroundColor() Color {
|
||||
if !isTTY(os.Stdout.Fd()) {
|
||||
return NoColor{}
|
||||
}
|
||||
|
||||
return foregroundColor()
|
||||
return output.ForegroundColor()
|
||||
}
|
||||
|
||||
// BackgroundColor returns the terminal's default background color.
|
||||
func BackgroundColor() Color {
|
||||
if !isTTY(os.Stdout.Fd()) {
|
||||
return NoColor{}
|
||||
}
|
||||
|
||||
return backgroundColor()
|
||||
return output.BackgroundColor()
|
||||
}
|
||||
|
||||
// HasDarkBackground returns whether terminal uses a dark-ish background.
|
||||
func HasDarkBackground() bool {
|
||||
c := ConvertToRGB(BackgroundColor())
|
||||
_, _, l := c.Hsl()
|
||||
return l < 0.5
|
||||
return output.HasDarkBackground()
|
||||
}
|
||||
|
||||
// EnvNoColor returns true if the environment variables explicitly disable color output
|
||||
// by setting NO_COLOR (https://no-color.org/)
|
||||
// or CLICOLOR/CLICOLOR_FORCE (https://bixense.com/clicolors/)
|
||||
// If NO_COLOR is set, this will return true, ignoring CLICOLOR/CLICOLOR_FORCE
|
||||
// If CLICOLOR=="0", it will be true only if CLICOLOR_FORCE is also "0" or is unset.
|
||||
func (o *Output) EnvNoColor() bool {
|
||||
return o.environ.Getenv("NO_COLOR") != "" || (o.environ.Getenv("CLICOLOR") == "0" && !o.cliColorForced())
|
||||
}
|
||||
|
||||
// EnvNoColor returns true if the environment variables explicitly disable color output
|
||||
|
@ -80,7 +65,7 @@ func HasDarkBackground() bool {
|
|||
// If NO_COLOR is set, this will return true, ignoring CLICOLOR/CLICOLOR_FORCE
|
||||
// If CLICOLOR=="0", it will be true only if CLICOLOR_FORCE is also "0" or is unset.
|
||||
func EnvNoColor() bool {
|
||||
return os.Getenv("NO_COLOR") != "" || (os.Getenv("CLICOLOR") == "0" && !cliColorForced())
|
||||
return output.EnvNoColor()
|
||||
}
|
||||
|
||||
// EnvColorProfile returns the color profile based on environment variables set
|
||||
|
@ -91,18 +76,27 @@ func EnvNoColor() bool {
|
|||
// If the terminal does not support any colors, but CLICOLOR_FORCE is set and not "0"
|
||||
// then the ANSI color profile will be returned.
|
||||
func EnvColorProfile() Profile {
|
||||
if EnvNoColor() {
|
||||
return output.EnvColorProfile()
|
||||
}
|
||||
|
||||
// EnvNoColor returns true if the environment variables explicitly disable color output
|
||||
// by setting NO_COLOR (https://no-color.org/)
|
||||
// or CLICOLOR/CLICOLOR_FORCE (https://bixense.com/clicolors/)
|
||||
// If NO_COLOR is set, this will return true, ignoring CLICOLOR/CLICOLOR_FORCE
|
||||
// If CLICOLOR=="0", it will be true only if CLICOLOR_FORCE is also "0" or is unset.
|
||||
func (o *Output) EnvColorProfile() Profile {
|
||||
if o.EnvNoColor() {
|
||||
return Ascii
|
||||
}
|
||||
p := ColorProfile()
|
||||
if cliColorForced() && p == Ascii {
|
||||
p := o.ColorProfile()
|
||||
if o.cliColorForced() && p == Ascii {
|
||||
return ANSI
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func cliColorForced() bool {
|
||||
if forced := os.Getenv("CLICOLOR_FORCE"); forced != "" {
|
||||
func (o *Output) cliColorForced() bool {
|
||||
if forced := o.environ.Getenv("CLICOLOR_FORCE"); forced != "" {
|
||||
return forced != "0"
|
||||
}
|
||||
return false
|
||||
|
|
|
@ -7,12 +7,12 @@ func colorProfile() Profile {
|
|||
return ANSI256
|
||||
}
|
||||
|
||||
func foregroundColor() Color {
|
||||
func (o Output) foregroundColor() Color {
|
||||
// default gray
|
||||
return ANSIColor(7)
|
||||
}
|
||||
|
||||
func backgroundColor() Color {
|
||||
func (o Output) backgroundColor() Color {
|
||||
// default black
|
||||
return ANSIColor(0)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ package termenv
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -18,9 +18,15 @@ const (
|
|||
OSCTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
func colorProfile() Profile {
|
||||
term := os.Getenv("TERM")
|
||||
colorTerm := os.Getenv("COLORTERM")
|
||||
// ColorProfile returns the supported color profile:
|
||||
// Ascii, ANSI, ANSI256, or TrueColor.
|
||||
func (o *Output) ColorProfile() Profile {
|
||||
if !o.isTTY() {
|
||||
return Ascii
|
||||
}
|
||||
|
||||
term := o.environ.Getenv("TERM")
|
||||
colorTerm := o.environ.Getenv("COLORTERM")
|
||||
|
||||
switch strings.ToLower(colorTerm) {
|
||||
case "24bit":
|
||||
|
@ -28,7 +34,7 @@ func colorProfile() Profile {
|
|||
case "truecolor":
|
||||
if strings.HasPrefix(term, "screen") {
|
||||
// tmux supports TrueColor, screen only ANSI256
|
||||
if os.Getenv("TERM_PROGRAM") != "tmux" {
|
||||
if o.environ.Getenv("TERM_PROGRAM") != "tmux" {
|
||||
return ANSI256
|
||||
}
|
||||
}
|
||||
|
@ -59,8 +65,8 @@ func colorProfile() Profile {
|
|||
return Ascii
|
||||
}
|
||||
|
||||
func foregroundColor() Color {
|
||||
s, err := termStatusReport(10)
|
||||
func (o Output) foregroundColor() Color {
|
||||
s, err := o.termStatusReport(10)
|
||||
if err == nil {
|
||||
c, err := xTermColor(s)
|
||||
if err == nil {
|
||||
|
@ -68,7 +74,7 @@ func foregroundColor() Color {
|
|||
}
|
||||
}
|
||||
|
||||
colorFGBG := os.Getenv("COLORFGBG")
|
||||
colorFGBG := o.environ.Getenv("COLORFGBG")
|
||||
if strings.Contains(colorFGBG, ";") {
|
||||
c := strings.Split(colorFGBG, ";")
|
||||
i, err := strconv.Atoi(c[0])
|
||||
|
@ -81,8 +87,8 @@ func foregroundColor() Color {
|
|||
return ANSIColor(7)
|
||||
}
|
||||
|
||||
func backgroundColor() Color {
|
||||
s, err := termStatusReport(11)
|
||||
func (o Output) backgroundColor() Color {
|
||||
s, err := o.termStatusReport(11)
|
||||
if err == nil {
|
||||
c, err := xTermColor(s)
|
||||
if err == nil {
|
||||
|
@ -90,7 +96,7 @@ func backgroundColor() Color {
|
|||
}
|
||||
}
|
||||
|
||||
colorFGBG := os.Getenv("COLORFGBG")
|
||||
colorFGBG := o.environ.Getenv("COLORFGBG")
|
||||
if strings.Contains(colorFGBG, ";") {
|
||||
c := strings.Split(colorFGBG, ";")
|
||||
i, err := strconv.Atoi(c[len(c)-1])
|
||||
|
@ -126,7 +132,7 @@ func waitForData(fd uintptr, timeout time.Duration) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func readNextByte(f *os.File) (byte, error) {
|
||||
func readNextByte(f File) (byte, error) {
|
||||
if err := waitForData(f.Fd(), OSCTimeout); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -145,9 +151,9 @@ func readNextByte(f *os.File) (byte, error) {
|
|||
}
|
||||
|
||||
// readNextResponse reads either an OSC response or a cursor position response:
|
||||
// * OSC response: "\x1b]11;rgb:1111/1111/1111\x1b\\"
|
||||
// * cursor position response: "\x1b[42;1R"
|
||||
func readNextResponse(fd *os.File) (response string, isOSC bool, err error) {
|
||||
// - OSC response: "\x1b]11;rgb:1111/1111/1111\x1b\\"
|
||||
// - cursor position response: "\x1b[42;1R"
|
||||
func readNextResponse(fd File) (response string, isOSC bool, err error) {
|
||||
start, err := readNextByte(fd)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
|
@ -182,7 +188,7 @@ func readNextResponse(fd *os.File) (response string, isOSC bool, err error) {
|
|||
}
|
||||
|
||||
for {
|
||||
b, err := readNextByte(os.Stdout)
|
||||
b, err := readNextByte(fd)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
@ -210,42 +216,48 @@ func readNextResponse(fd *os.File) (response string, isOSC bool, err error) {
|
|||
return "", false, ErrStatusReport
|
||||
}
|
||||
|
||||
func termStatusReport(sequence int) (string, error) {
|
||||
func (o Output) termStatusReport(sequence int) (string, error) {
|
||||
// screen/tmux can't support OSC, because they can be connected to multiple
|
||||
// terminals concurrently.
|
||||
term := os.Getenv("TERM")
|
||||
if strings.HasPrefix(term, "screen") {
|
||||
term := o.environ.Getenv("TERM")
|
||||
if strings.HasPrefix(term, "screen") || strings.HasPrefix(term, "tmux") {
|
||||
return "", ErrStatusReport
|
||||
}
|
||||
|
||||
tty := o.TTY()
|
||||
if tty == nil {
|
||||
return "", ErrStatusReport
|
||||
}
|
||||
|
||||
fd := int(tty.Fd())
|
||||
// if in background, we can't control the terminal
|
||||
if !isForeground(unix.Stdout) {
|
||||
if !isForeground(fd) {
|
||||
return "", ErrStatusReport
|
||||
}
|
||||
|
||||
t, err := unix.IoctlGetTermios(unix.Stdout, tcgetattr)
|
||||
t, err := unix.IoctlGetTermios(fd, tcgetattr)
|
||||
if err != nil {
|
||||
return "", ErrStatusReport
|
||||
return "", fmt.Errorf("%s: %s", ErrStatusReport, err)
|
||||
}
|
||||
defer unix.IoctlSetTermios(unix.Stdout, tcsetattr, t) //nolint:errcheck
|
||||
defer unix.IoctlSetTermios(fd, tcsetattr, t) //nolint:errcheck
|
||||
|
||||
noecho := *t
|
||||
noecho.Lflag = noecho.Lflag &^ unix.ECHO
|
||||
noecho.Lflag = noecho.Lflag &^ unix.ICANON
|
||||
if err := unix.IoctlSetTermios(unix.Stdout, tcsetattr, &noecho); err != nil {
|
||||
return "", ErrStatusReport
|
||||
if err := unix.IoctlSetTermios(fd, tcsetattr, &noecho); err != nil {
|
||||
return "", fmt.Errorf("%s: %s", ErrStatusReport, err)
|
||||
}
|
||||
|
||||
// first, send OSC query, which is ignored by terminal which do not support it
|
||||
fmt.Printf("\033]%d;?\033\\", sequence)
|
||||
fmt.Fprintf(tty, "\033]%d;?\033\\", sequence)
|
||||
|
||||
// then, query cursor position, should be supported by all terminals
|
||||
fmt.Printf("\033[6n")
|
||||
fmt.Fprintf(tty, "\033[6n")
|
||||
|
||||
// read the next response
|
||||
res, isOSC, err := readNextResponse(os.Stdout)
|
||||
res, isOSC, err := readNextResponse(tty)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("%s: %s", ErrStatusReport, err)
|
||||
}
|
||||
|
||||
// if this is not OSC response, then the terminal does not support it
|
||||
|
@ -254,7 +266,7 @@ func termStatusReport(sequence int) (string, error) {
|
|||
}
|
||||
|
||||
// read the cursor query response next and discard the result
|
||||
_, _, err = readNextResponse(os.Stdout)
|
||||
_, _, err = readNextResponse(tty)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -262,3 +274,11 @@ func termStatusReport(sequence int) (string, error) {
|
|||
// fmt.Println("Rcvd", res[1:])
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// EnableVirtualTerminalProcessing enables virtual terminal processing on
|
||||
// Windows for w and returns a function that restores w to its previous state.
|
||||
// On non-Windows platforms, or if w does not refer to a terminal, then it
|
||||
// returns a non-nil no-op function and no error.
|
||||
func EnableVirtualTerminalProcessing(w io.Writer) (func() error, error) {
|
||||
return func() error { return nil }, nil
|
||||
}
|
||||
|
|
|
@ -4,22 +4,26 @@
|
|||
package termenv
|
||||
|
||||
import (
|
||||
"os"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func colorProfile() Profile {
|
||||
if os.Getenv("ConEmuANSI") == "ON" {
|
||||
func (o *Output) ColorProfile() Profile {
|
||||
if !o.isTTY() {
|
||||
return Ascii
|
||||
}
|
||||
|
||||
if o.environ.Getenv("ConEmuANSI") == "ON" {
|
||||
return TrueColor
|
||||
}
|
||||
|
||||
winVersion, _, buildNumber := windows.RtlGetNtVersionNumbers()
|
||||
if buildNumber < 10586 || winVersion < 10 {
|
||||
// No ANSI support before Windows 10 build 10586.
|
||||
if os.Getenv("ANSICON") != "" {
|
||||
conVersion := os.Getenv("ANSICON_VER")
|
||||
if o.environ.Getenv("ANSICON") != "" {
|
||||
conVersion := o.environ.Getenv("ANSICON_VER")
|
||||
cv, err := strconv.ParseInt(conVersion, 10, 64)
|
||||
if err != nil || cv < 181 {
|
||||
// No 8 bit color support before v1.81 release.
|
||||
|
@ -39,12 +43,12 @@ func colorProfile() Profile {
|
|||
return TrueColor
|
||||
}
|
||||
|
||||
func foregroundColor() Color {
|
||||
func (o Output) foregroundColor() Color {
|
||||
// default gray
|
||||
return ANSIColor(7)
|
||||
}
|
||||
|
||||
func backgroundColor() Color {
|
||||
func (o Output) backgroundColor() Color {
|
||||
// default black
|
||||
return ANSIColor(0)
|
||||
}
|
||||
|
@ -87,3 +91,49 @@ func RestoreWindowsConsole(mode uint32) error {
|
|||
|
||||
return windows.SetConsoleMode(handle, mode)
|
||||
}
|
||||
|
||||
// EnableVirtualTerminalProcessing enables virtual terminal processing on
|
||||
// Windows for o and returns a function that restores o to its previous state.
|
||||
// On non-Windows platforms, or if o does not refer to a terminal, then it
|
||||
// returns a non-nil no-op function and no error.
|
||||
func EnableVirtualTerminalProcessing(o *Output) (restoreFunc func() error, err error) {
|
||||
// There is nothing to restore until we set the console mode.
|
||||
restoreFunc = func() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If o is not a tty, then there is nothing to do.
|
||||
tty := o.TTY()
|
||||
if tty == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the current console mode. If there is an error, assume that o is not
|
||||
// a terminal, discard the error, and return.
|
||||
var mode uint32
|
||||
if err2 := windows.GetConsoleMode(windows.Handle(tty.Fd()), &mode); err2 != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If virtual terminal processing is already set, then there is nothing to
|
||||
// do and nothing to restore.
|
||||
if mode&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING == windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING {
|
||||
return
|
||||
}
|
||||
|
||||
// Enable virtual terminal processing. See
|
||||
// https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
|
||||
if err2 := windows.SetConsoleMode(windows.Handle(tty.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err2 != nil {
|
||||
err = fmt.Errorf("windows.SetConsoleMode: %w", err2)
|
||||
return
|
||||
}
|
||||
|
||||
// Set the restore function. We maintain a reference to the tty in the
|
||||
// closure (rather than just its handle) to ensure that the tty is not
|
||||
// closed by a finalizer.
|
||||
restoreFunc = func() error {
|
||||
return windows.SetConsoleMode(windows.Handle(tty.Fd()), mode)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
crashers
|
||||
suppressions
|
||||
netaddr-fuzz.zip
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "corpus"]
|
||||
path = corpus
|
||||
url = https://github.com/inetaf/netaddr-corpus.git
|
|
@ -0,0 +1,4 @@
|
|||
Alex Willmer <alex@moreati.org.uk>
|
||||
Matt Layher <mdlayher@gmail.com>
|
||||
Tailscale Inc.
|
||||
Tobias Klauser <tklauser@distanz.ch>
|
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2020 The Inet.af AUTHORS. 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 Tailscale Inc. 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.
|
|
@ -0,0 +1,26 @@
|
|||
# netipx [![Test Status](https://github.com/go4org/netipx/workflows/Linux/badge.svg)](https://github.com/go4org/netipx/actions) [![Go Reference](https://pkg.go.dev/badge/go4.org/netipx.svg)](https://pkg.go.dev/go4.org/netipx)
|
||||
|
||||
## What
|
||||
|
||||
This is a package containing the bits of the old `inet.af/netaddr` package that didn't make it
|
||||
into Go 1.18's `net/netip` standard library package.
|
||||
|
||||
As background, see:
|
||||
|
||||
* https://github.com/inetaf/netaddr/ (now deprecated)
|
||||
* https://tailscale.com/blog/netaddr-new-ip-type-for-go/ - blog post about why the package came to be originally
|
||||
* https://go.dev/doc/go1.18#netip - Go 1.18 release notes
|
||||
|
||||
This package requires Go 1.18+ to use and complements the `net/netip`.
|
||||
|
||||
## FAQ
|
||||
|
||||
**Why's it no longer under `inet.af`?** Since that joke started, that
|
||||
TLD is now under control of the Taliban. (Yes, we should've known
|
||||
better. We'd even previously scolded people for relying on
|
||||
questionable ccTLDs. Whoops.)
|
||||
|
||||
**Will this stuff make it into the standard library?** [Maybe](https://github.com/golang/go/issues/53236).
|
||||
We'll see.
|
||||
|
||||
|
|
@ -0,0 +1,498 @@
|
|||
// Copyright 2020 The Inet.Af AUTHORS. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package netipx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IPSetBuilder builds an immutable IPSet.
|
||||
//
|
||||
// The zero value is a valid value representing a set of no IPs.
|
||||
//
|
||||
// The Add and Remove methods add or remove IPs to/from the set.
|
||||
// Removals only affect the current membership of the set, so in
|
||||
// general Adds should be called first. Input ranges may overlap in
|
||||
// any way.
|
||||
//
|
||||
// Most IPSetBuilder methods do not return errors.
|
||||
// Instead, errors are accumulated and reported by IPSetBuilder.IPSet.
|
||||
type IPSetBuilder struct {
|
||||
// in are the ranges in the set.
|
||||
in []IPRange
|
||||
|
||||
// out are the ranges to be removed from 'in'.
|
||||
out []IPRange
|
||||
|
||||
// errs are errors accumulated during construction.
|
||||
errs multiErr
|
||||
}
|
||||
|
||||
// normalize normalizes s: s.in becomes the minimal sorted list of
|
||||
// ranges required to describe s, and s.out becomes empty.
|
||||
func (s *IPSetBuilder) normalize() {
|
||||
const debug = false
|
||||
if debug {
|
||||
debugf("ranges start in=%v out=%v", s.in, s.out)
|
||||
}
|
||||
in, ok := mergeIPRanges(s.in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
out, ok := mergeIPRanges(s.out)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if debug {
|
||||
debugf("ranges sort in=%v out=%v", in, out)
|
||||
}
|
||||
|
||||
// in and out are sorted in ascending range order, and have no
|
||||
// overlaps within each other. We can run a merge of the two lists
|
||||
// in one pass.
|
||||
|
||||
min := make([]IPRange, 0, len(in))
|
||||
for len(in) > 0 && len(out) > 0 {
|
||||
rin, rout := in[0], out[0]
|
||||
if debug {
|
||||
debugf("step in=%v out=%v", rin, rout)
|
||||
}
|
||||
|
||||
switch {
|
||||
case !rout.IsValid() || !rin.IsValid():
|
||||
// mergeIPRanges should have prevented invalid ranges from
|
||||
// sneaking in.
|
||||
panic("invalid IPRanges during Ranges merge")
|
||||
case rout.entirelyBefore(rin):
|
||||
// "out" is entirely before "in".
|
||||
//
|
||||
// out in
|
||||
// f-------t f-------t
|
||||
out = out[1:]
|
||||
if debug {
|
||||
debugf("out before in; drop out")
|
||||
}
|
||||
case rin.entirelyBefore(rout):
|
||||
// "in" is entirely before "out".
|
||||
//
|
||||
// in out
|
||||
// f------t f-------t
|
||||
min = append(min, rin)
|
||||
in = in[1:]
|
||||
if debug {
|
||||
debugf("in before out; append in")
|
||||
debugf("min=%v", min)
|
||||
}
|
||||
case rin.coveredBy(rout):
|
||||
// "out" entirely covers "in".
|
||||
//
|
||||
// out
|
||||
// f-------------t
|
||||
// f------t
|
||||
// in
|
||||
in = in[1:]
|
||||
if debug {
|
||||
debugf("in inside out; drop in")
|
||||
}
|
||||
case rout.inMiddleOf(rin):
|
||||
// "in" entirely covers "out".
|
||||
//
|
||||
// in
|
||||
// f-------------t
|
||||
// f------t
|
||||
// out
|
||||
min = append(min, IPRange{from: rin.from, to: AddrPrior(rout.from)})
|
||||
// Adjust in[0], not ir, because we want to consider the
|
||||
// mutated range on the next iteration.
|
||||
in[0].from = rout.to.Next()
|
||||
out = out[1:]
|
||||
if debug {
|
||||
debugf("out inside in; split in, append first in, drop out, adjust second in")
|
||||
debugf("min=%v", min)
|
||||
}
|
||||
case rout.overlapsStartOf(rin):
|
||||
// "out" overlaps start of "in".
|
||||
//
|
||||
// out
|
||||
// f------t
|
||||
// f------t
|
||||
// in
|
||||
in[0].from = rout.to.Next()
|
||||
// Can't move ir onto min yet, another later out might
|
||||
// trim it further. Just discard or and continue.
|
||||
out = out[1:]
|
||||
if debug {
|
||||
debugf("out cuts start of in; adjust in, drop out")
|
||||
}
|
||||
case rout.overlapsEndOf(rin):
|
||||
// "out" overlaps end of "in".
|
||||
//
|
||||
// out
|
||||
// f------t
|
||||
// f------t
|
||||
// in
|
||||
min = append(min, IPRange{from: rin.from, to: AddrPrior(rout.from)})
|
||||
in = in[1:]
|
||||
if debug {
|
||||
debugf("merge out cuts end of in; append shortened in")
|
||||
debugf("min=%v", min)
|
||||
}
|
||||
default:
|
||||
// The above should account for all combinations of in and
|
||||
// out overlapping, but insert a panic to be sure.
|
||||
panic("unexpected additional overlap scenario")
|
||||
}
|
||||
}
|
||||
if len(in) > 0 {
|
||||
// Ran out of removals before the end of in.
|
||||
min = append(min, in...)
|
||||
if debug {
|
||||
debugf("min=%v", min)
|
||||
}
|
||||
}
|
||||
|
||||
s.in = min
|
||||
s.out = nil
|
||||
}
|
||||
|
||||
// Clone returns a copy of s that shares no memory with s.
|
||||
func (s *IPSetBuilder) Clone() *IPSetBuilder {
|
||||
return &IPSetBuilder{
|
||||
in: append([]IPRange(nil), s.in...),
|
||||
out: append([]IPRange(nil), s.out...),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IPSetBuilder) addError(msg string, args ...interface{}) {
|
||||
se := new(stacktraceErr)
|
||||
// Skip three frames: runtime.Callers, addError, and the IPSetBuilder
|
||||
// method that called addError (such as IPSetBuilder.Add).
|
||||
// The resulting stack trace ends at the line in the user's
|
||||
// code where they called into netaddr.
|
||||
n := runtime.Callers(3, se.pcs[:])
|
||||
se.at = se.pcs[:n]
|
||||
se.err = fmt.Errorf(msg, args...)
|
||||
s.errs = append(s.errs, se)
|
||||
}
|
||||
|
||||
// Add adds ip to s.
|
||||
func (s *IPSetBuilder) Add(ip netip.Addr) {
|
||||
if !ip.IsValid() {
|
||||
s.addError("Add(IP{})")
|
||||
return
|
||||
}
|
||||
s.AddRange(IPRangeFrom(ip, ip))
|
||||
}
|
||||
|
||||
// AddPrefix adds all IPs in p to s.
|
||||
func (s *IPSetBuilder) AddPrefix(p netip.Prefix) {
|
||||
if r := RangeOfPrefix(p); r.IsValid() {
|
||||
s.AddRange(r)
|
||||
} else {
|
||||
s.addError("AddPrefix(%v/%v)", p.Addr(), p.Bits())
|
||||
}
|
||||
}
|
||||
|
||||
// AddRange adds r to s.
|
||||
// If r is not Valid, AddRange does nothing.
|
||||
func (s *IPSetBuilder) AddRange(r IPRange) {
|
||||
if !r.IsValid() {
|
||||
s.addError("AddRange(%v-%v)", r.From(), r.To())
|
||||
return
|
||||
}
|
||||
// If there are any removals (s.out), then we need to compact the set
|
||||
// first to get the order right.
|
||||
if len(s.out) > 0 {
|
||||
s.normalize()
|
||||
}
|
||||
s.in = append(s.in, r)
|
||||
}
|
||||
|
||||
// AddSet adds all IPs in b to s.
|
||||
func (s *IPSetBuilder) AddSet(b *IPSet) {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
for _, r := range b.rr {
|
||||
s.AddRange(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removes ip from s.
|
||||
func (s *IPSetBuilder) Remove(ip netip.Addr) {
|
||||
if !ip.IsValid() {
|
||||
s.addError("Remove(IP{})")
|
||||
} else {
|
||||
s.RemoveRange(IPRangeFrom(ip, ip))
|
||||
}
|
||||
}
|
||||
|
||||
// RemovePrefix removes all IPs in p from s.
|
||||
func (s *IPSetBuilder) RemovePrefix(p netip.Prefix) {
|
||||
if r := RangeOfPrefix(p); r.IsValid() {
|
||||
s.RemoveRange(r)
|
||||
} else {
|
||||
s.addError("RemovePrefix(%v/%v)", p.Addr(), p.Bits())
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveRange removes all IPs in r from s.
|
||||
func (s *IPSetBuilder) RemoveRange(r IPRange) {
|
||||
if r.IsValid() {
|
||||
s.out = append(s.out, r)
|
||||
} else {
|
||||
s.addError("RemoveRange(%v-%v)", r.From(), r.To())
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveSet removes all IPs in o from s.
|
||||
func (s *IPSetBuilder) RemoveSet(b *IPSet) {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
for _, r := range b.rr {
|
||||
s.RemoveRange(r)
|
||||
}
|
||||
}
|
||||
|
||||
// removeBuilder removes all IPs in b from s.
|
||||
func (s *IPSetBuilder) removeBuilder(b *IPSetBuilder) {
|
||||
b.normalize()
|
||||
for _, r := range b.in {
|
||||
s.RemoveRange(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Complement updates s to contain the complement of its current
|
||||
// contents.
|
||||
func (s *IPSetBuilder) Complement() {
|
||||
s.normalize()
|
||||
s.out = s.in
|
||||
s.in = []IPRange{
|
||||
RangeOfPrefix(netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 0)),
|
||||
RangeOfPrefix(netip.PrefixFrom(netip.IPv6Unspecified(), 0)),
|
||||
}
|
||||
}
|
||||
|
||||
// Intersect updates s to the set intersection of s and b.
|
||||
func (s *IPSetBuilder) Intersect(b *IPSet) {
|
||||
var o IPSetBuilder
|
||||
o.Complement()
|
||||
o.RemoveSet(b)
|
||||
s.removeBuilder(&o)
|
||||
}
|
||||
|
||||
func discardf(format string, args ...interface{}) {}
|
||||
|
||||
// debugf is reassigned by tests.
|
||||
var debugf = discardf
|
||||
|
||||
// IPSet returns an immutable IPSet representing the current state of s.
|
||||
//
|
||||
// Most IPSetBuilder methods do not return errors.
|
||||
// Rather, the builder ignores any invalid inputs (such as an invalid IPPrefix),
|
||||
// and accumulates a list of any such errors that it encountered.
|
||||
//
|
||||
// IPSet also reports any such accumulated errors.
|
||||
// Even if the returned error is non-nil, the returned IPSet is usable
|
||||
// and contains all modifications made with valid inputs.
|
||||
//
|
||||
// The builder remains usable after calling IPSet.
|
||||
// Calling IPSet clears any accumulated errors.
|
||||
func (s *IPSetBuilder) IPSet() (*IPSet, error) {
|
||||
s.normalize()
|
||||
ret := &IPSet{
|
||||
rr: append([]IPRange{}, s.in...),
|
||||
}
|
||||
if len(s.errs) == 0 {
|
||||
return ret, nil
|
||||
} else {
|
||||
errs := s.errs
|
||||
s.errs = nil
|
||||
return ret, errs
|
||||
}
|
||||
}
|
||||
|
||||
// IPSet represents a set of IP addresses.
|
||||
//
|
||||
// IPSet is safe for concurrent use.
|
||||
// The zero value is a valid value representing a set of no IPs.
|
||||
// Use IPSetBuilder to construct IPSets.
|
||||
type IPSet struct {
|
||||
// rr is the set of IPs that belong to this IPSet. The IPRanges
|
||||
// are normalized according to IPSetBuilder.normalize, meaning
|
||||
// they are a sorted, minimal representation (no overlapping
|
||||
// ranges, no contiguous ranges). The implementation of various
|
||||
// methods rely on this property.
|
||||
rr []IPRange
|
||||
}
|
||||
|
||||
// Ranges returns the minimum and sorted set of IP
|
||||
// ranges that covers s.
|
||||
func (s *IPSet) Ranges() []IPRange {
|
||||
return append([]IPRange{}, s.rr...)
|
||||
}
|
||||
|
||||
// Prefixes returns the minimum and sorted set of IP prefixes
|
||||
// that covers s.
|
||||
func (s *IPSet) Prefixes() []netip.Prefix {
|
||||
out := make([]netip.Prefix, 0, len(s.rr))
|
||||
for _, r := range s.rr {
|
||||
out = append(out, r.Prefixes()...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Equal reports whether s and o represent the same set of IP
|
||||
// addresses.
|
||||
func (s *IPSet) Equal(o *IPSet) bool {
|
||||
if len(s.rr) != len(o.rr) {
|
||||
return false
|
||||
}
|
||||
for i := range s.rr {
|
||||
if s.rr[i] != o.rr[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Contains reports whether ip is in s.
|
||||
// If ip has an IPv6 zone, Contains returns false,
|
||||
// because IPSets do not track zones.
|
||||
func (s *IPSet) Contains(ip netip.Addr) bool {
|
||||
if ip.Zone() != "" {
|
||||
return false
|
||||
}
|
||||
// TODO: data structure permitting more efficient lookups:
|
||||
// https://github.com/inetaf/netaddr/issues/139
|
||||
i := sort.Search(len(s.rr), func(i int) bool {
|
||||
return ip.Less(s.rr[i].from)
|
||||
})
|
||||
if i == 0 {
|
||||
return false
|
||||
}
|
||||
i--
|
||||
return s.rr[i].contains(ip)
|
||||
}
|
||||
|
||||
// ContainsRange reports whether all IPs in r are in s.
|
||||
func (s *IPSet) ContainsRange(r IPRange) bool {
|
||||
for _, x := range s.rr {
|
||||
if r.coveredBy(x) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ContainsPrefix reports whether all IPs in p are in s.
|
||||
func (s *IPSet) ContainsPrefix(p netip.Prefix) bool {
|
||||
return s.ContainsRange(RangeOfPrefix(p))
|
||||
}
|
||||
|
||||
// Overlaps reports whether any IP in b is also in s.
|
||||
func (s *IPSet) Overlaps(b *IPSet) bool {
|
||||
// TODO: sorted ranges lets us do this in O(n+m)
|
||||
for _, r := range s.rr {
|
||||
for _, or := range b.rr {
|
||||
if r.Overlaps(or) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// OverlapsRange reports whether any IP in r is also in s.
|
||||
func (s *IPSet) OverlapsRange(r IPRange) bool {
|
||||
// TODO: sorted ranges lets us do this more efficiently.
|
||||
for _, x := range s.rr {
|
||||
if x.Overlaps(r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// OverlapsPrefix reports whether any IP in p is also in s.
|
||||
func (s *IPSet) OverlapsPrefix(p netip.Prefix) bool {
|
||||
return s.OverlapsRange(RangeOfPrefix(p))
|
||||
}
|
||||
|
||||
// RemoveFreePrefix splits s into a Prefix of length bitLen and a new
|
||||
// IPSet with that prefix removed.
|
||||
//
|
||||
// If no contiguous prefix of length bitLen exists in s,
|
||||
// RemoveFreePrefix returns ok=false.
|
||||
func (s *IPSet) RemoveFreePrefix(bitLen uint8) (p netip.Prefix, newSet *IPSet, ok bool) {
|
||||
var bestFit netip.Prefix
|
||||
for _, r := range s.rr {
|
||||
for _, prefix := range r.Prefixes() {
|
||||
if uint8(prefix.Bits()) > bitLen {
|
||||
continue
|
||||
}
|
||||
if !bestFit.Addr().IsValid() || prefix.Bits() > bestFit.Bits() {
|
||||
bestFit = prefix
|
||||
if uint8(bestFit.Bits()) == bitLen {
|
||||
// exact match, done.
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !bestFit.Addr().IsValid() {
|
||||
return netip.Prefix{}, s, false
|
||||
}
|
||||
|
||||
prefix := netip.PrefixFrom(bestFit.Addr(), int(bitLen))
|
||||
|
||||
var b IPSetBuilder
|
||||
b.AddSet(s)
|
||||
b.RemovePrefix(prefix)
|
||||
newSet, _ = b.IPSet()
|
||||
return prefix, newSet, true
|
||||
}
|
||||
|
||||
type multiErr []error
|
||||
|
||||
func (e multiErr) Error() string {
|
||||
var ret []string
|
||||
for _, err := range e {
|
||||
ret = append(ret, err.Error())
|
||||
}
|
||||
return strings.Join(ret, "; ")
|
||||
}
|
||||
|
||||
// A stacktraceErr combines an error with a stack trace.
|
||||
type stacktraceErr struct {
|
||||
pcs [16]uintptr // preallocated array of PCs
|
||||
at []uintptr // stack trace whence the error
|
||||
err error // underlying error
|
||||
}
|
||||
|
||||
func (e *stacktraceErr) Error() string {
|
||||
frames := runtime.CallersFrames(e.at)
|
||||
buf := new(strings.Builder)
|
||||
buf.WriteString(e.err.Error())
|
||||
buf.WriteString(" @ ")
|
||||
for {
|
||||
frame, more := frames.Next()
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
fmt.Fprintf(buf, "%s:%d ", frame.File, frame.Line)
|
||||
}
|
||||
return strings.TrimSpace(buf.String())
|
||||
}
|
||||
|
||||
func (e *stacktraceErr) Unwrap() error {
|
||||
return e.err
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
// Copyright 2021 The Inet.Af AUTHORS. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package netipx
|
||||
|
||||
// mask6 are bitmasks with the topmost n bits of a
|
||||
// 128-bit number, where n is the array index.
|
||||
//
|
||||
// generated with https://play.golang.org/p/64XKxaUSa_9
|
||||
var mask6 = [...]uint128{
|
||||
0: {0x0000000000000000, 0x0000000000000000},
|
||||
1: {0x8000000000000000, 0x0000000000000000},
|
||||
2: {0xc000000000000000, 0x0000000000000000},
|
||||
3: {0xe000000000000000, 0x0000000000000000},
|
||||
4: {0xf000000000000000, 0x0000000000000000},
|
||||
5: {0xf800000000000000, 0x0000000000000000},
|
||||
6: {0xfc00000000000000, 0x0000000000000000},
|
||||
7: {0xfe00000000000000, 0x0000000000000000},
|
||||
8: {0xff00000000000000, 0x0000000000000000},
|
||||
9: {0xff80000000000000, 0x0000000000000000},
|
||||
10: {0xffc0000000000000, 0x0000000000000000},
|
||||
11: {0xffe0000000000000, 0x0000000000000000},
|
||||
12: {0xfff0000000000000, 0x0000000000000000},
|
||||
13: {0xfff8000000000000, 0x0000000000000000},
|
||||
14: {0xfffc000000000000, 0x0000000000000000},
|
||||
15: {0xfffe000000000000, 0x0000000000000000},
|
||||
16: {0xffff000000000000, 0x0000000000000000},
|
||||
17: {0xffff800000000000, 0x0000000000000000},
|
||||
18: {0xffffc00000000000, 0x0000000000000000},
|
||||
19: {0xffffe00000000000, 0x0000000000000000},
|
||||
20: {0xfffff00000000000, 0x0000000000000000},
|
||||
21: {0xfffff80000000000, 0x0000000000000000},
|
||||
22: {0xfffffc0000000000, 0x0000000000000000},
|
||||
23: {0xfffffe0000000000, 0x0000000000000000},
|
||||
24: {0xffffff0000000000, 0x0000000000000000},
|
||||
25: {0xffffff8000000000, 0x0000000000000000},
|
||||
26: {0xffffffc000000000, 0x0000000000000000},
|
||||
27: {0xffffffe000000000, 0x0000000000000000},
|
||||
28: {0xfffffff000000000, 0x0000000000000000},
|
||||
29: {0xfffffff800000000, 0x0000000000000000},
|
||||
30: {0xfffffffc00000000, 0x0000000000000000},
|
||||
31: {0xfffffffe00000000, 0x0000000000000000},
|
||||
32: {0xffffffff00000000, 0x0000000000000000},
|
||||
33: {0xffffffff80000000, 0x0000000000000000},
|
||||
34: {0xffffffffc0000000, 0x0000000000000000},
|
||||
35: {0xffffffffe0000000, 0x0000000000000000},
|
||||
36: {0xfffffffff0000000, 0x0000000000000000},
|
||||
37: {0xfffffffff8000000, 0x0000000000000000},
|
||||
38: {0xfffffffffc000000, 0x0000000000000000},
|
||||
39: {0xfffffffffe000000, 0x0000000000000000},
|
||||
40: {0xffffffffff000000, 0x0000000000000000},
|
||||
41: {0xffffffffff800000, 0x0000000000000000},
|
||||
42: {0xffffffffffc00000, 0x0000000000000000},
|
||||
43: {0xffffffffffe00000, 0x0000000000000000},
|
||||
44: {0xfffffffffff00000, 0x0000000000000000},
|
||||
45: {0xfffffffffff80000, 0x0000000000000000},
|
||||
46: {0xfffffffffffc0000, 0x0000000000000000},
|
||||
47: {0xfffffffffffe0000, 0x0000000000000000},
|
||||
48: {0xffffffffffff0000, 0x0000000000000000},
|
||||
49: {0xffffffffffff8000, 0x0000000000000000},
|
||||
50: {0xffffffffffffc000, 0x0000000000000000},
|
||||
51: {0xffffffffffffe000, 0x0000000000000000},
|
||||
52: {0xfffffffffffff000, 0x0000000000000000},
|
||||
53: {0xfffffffffffff800, 0x0000000000000000},
|
||||
54: {0xfffffffffffffc00, 0x0000000000000000},
|
||||
55: {0xfffffffffffffe00, 0x0000000000000000},
|
||||
56: {0xffffffffffffff00, 0x0000000000000000},
|
||||
57: {0xffffffffffffff80, 0x0000000000000000},
|
||||
58: {0xffffffffffffffc0, 0x0000000000000000},
|
||||
59: {0xffffffffffffffe0, 0x0000000000000000},
|
||||
60: {0xfffffffffffffff0, 0x0000000000000000},
|
||||
61: {0xfffffffffffffff8, 0x0000000000000000},
|
||||
62: {0xfffffffffffffffc, 0x0000000000000000},
|
||||
63: {0xfffffffffffffffe, 0x0000000000000000},
|
||||
64: {0xffffffffffffffff, 0x0000000000000000},
|
||||
65: {0xffffffffffffffff, 0x8000000000000000},
|
||||
66: {0xffffffffffffffff, 0xc000000000000000},
|
||||
67: {0xffffffffffffffff, 0xe000000000000000},
|
||||
68: {0xffffffffffffffff, 0xf000000000000000},
|
||||
69: {0xffffffffffffffff, 0xf800000000000000},
|
||||
70: {0xffffffffffffffff, 0xfc00000000000000},
|
||||
71: {0xffffffffffffffff, 0xfe00000000000000},
|
||||
72: {0xffffffffffffffff, 0xff00000000000000},
|
||||
73: {0xffffffffffffffff, 0xff80000000000000},
|
||||
74: {0xffffffffffffffff, 0xffc0000000000000},
|
||||
75: {0xffffffffffffffff, 0xffe0000000000000},
|
||||
76: {0xffffffffffffffff, 0xfff0000000000000},
|
||||
77: {0xffffffffffffffff, 0xfff8000000000000},
|
||||
78: {0xffffffffffffffff, 0xfffc000000000000},
|
||||
79: {0xffffffffffffffff, 0xfffe000000000000},
|
||||
80: {0xffffffffffffffff, 0xffff000000000000},
|
||||
81: {0xffffffffffffffff, 0xffff800000000000},
|
||||
82: {0xffffffffffffffff, 0xffffc00000000000},
|
||||
83: {0xffffffffffffffff, 0xffffe00000000000},
|
||||
84: {0xffffffffffffffff, 0xfffff00000000000},
|
||||
85: {0xffffffffffffffff, 0xfffff80000000000},
|
||||
86: {0xffffffffffffffff, 0xfffffc0000000000},
|
||||
87: {0xffffffffffffffff, 0xfffffe0000000000},
|
||||
88: {0xffffffffffffffff, 0xffffff0000000000},
|
||||
89: {0xffffffffffffffff, 0xffffff8000000000},
|
||||
90: {0xffffffffffffffff, 0xffffffc000000000},
|
||||
91: {0xffffffffffffffff, 0xffffffe000000000},
|
||||
92: {0xffffffffffffffff, 0xfffffff000000000},
|
||||
93: {0xffffffffffffffff, 0xfffffff800000000},
|
||||
94: {0xffffffffffffffff, 0xfffffffc00000000},
|
||||
95: {0xffffffffffffffff, 0xfffffffe00000000},
|
||||
96: {0xffffffffffffffff, 0xffffffff00000000},
|
||||
97: {0xffffffffffffffff, 0xffffffff80000000},
|
||||
98: {0xffffffffffffffff, 0xffffffffc0000000},
|
||||
99: {0xffffffffffffffff, 0xffffffffe0000000},
|
||||
100: {0xffffffffffffffff, 0xfffffffff0000000},
|
||||
101: {0xffffffffffffffff, 0xfffffffff8000000},
|
||||
102: {0xffffffffffffffff, 0xfffffffffc000000},
|
||||
103: {0xffffffffffffffff, 0xfffffffffe000000},
|
||||
104: {0xffffffffffffffff, 0xffffffffff000000},
|
||||
105: {0xffffffffffffffff, 0xffffffffff800000},
|
||||
106: {0xffffffffffffffff, 0xffffffffffc00000},
|
||||
107: {0xffffffffffffffff, 0xffffffffffe00000},
|
||||
108: {0xffffffffffffffff, 0xfffffffffff00000},
|
||||
109: {0xffffffffffffffff, 0xfffffffffff80000},
|
||||
110: {0xffffffffffffffff, 0xfffffffffffc0000},
|
||||
111: {0xffffffffffffffff, 0xfffffffffffe0000},
|
||||
112: {0xffffffffffffffff, 0xffffffffffff0000},
|
||||
113: {0xffffffffffffffff, 0xffffffffffff8000},
|
||||
114: {0xffffffffffffffff, 0xffffffffffffc000},
|
||||
115: {0xffffffffffffffff, 0xffffffffffffe000},
|
||||
116: {0xffffffffffffffff, 0xfffffffffffff000},
|
||||
117: {0xffffffffffffffff, 0xfffffffffffff800},
|
||||
118: {0xffffffffffffffff, 0xfffffffffffffc00},
|
||||
119: {0xffffffffffffffff, 0xfffffffffffffe00},
|
||||
120: {0xffffffffffffffff, 0xffffffffffffff00},
|
||||
121: {0xffffffffffffffff, 0xffffffffffffff80},
|
||||
122: {0xffffffffffffffff, 0xffffffffffffffc0},
|
||||
123: {0xffffffffffffffff, 0xffffffffffffffe0},
|
||||
124: {0xffffffffffffffff, 0xfffffffffffffff0},
|
||||
125: {0xffffffffffffffff, 0xfffffffffffffff8},
|
||||
126: {0xffffffffffffffff, 0xfffffffffffffffc},
|
||||
127: {0xffffffffffffffff, 0xfffffffffffffffe},
|
||||
128: {0xffffffffffffffff, 0xffffffffffffffff},
|
||||
}
|
|
@ -0,0 +1,547 @@
|
|||
// Copyright 2020 The Inet.Af AUTHORS. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package netipx contains code and types that were left behind when
|
||||
// the old inet.af/netaddr package moved to the standard library in Go
|
||||
// 1.18 as net/netip.
|
||||
package netipx // import "go4.org/netipx"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FromStdIP returns an IP from the standard library's IP type.
|
||||
//
|
||||
// If std is invalid, ok is false.
|
||||
//
|
||||
// FromStdIP implicitly unmaps IPv6-mapped IPv4 addresses. That is, if
|
||||
// len(std) == 16 and contains an IPv4 address, only the IPv4 part is
|
||||
// returned, without the IPv6 wrapper. This is the common form returned by
|
||||
// the standard library's ParseIP: https://play.golang.org/p/qdjylUkKWxl.
|
||||
// To convert a standard library IP without the implicit unmapping, use
|
||||
// netip.AddrFromSlice.
|
||||
func FromStdIP(std net.IP) (ip netip.Addr, ok bool) {
|
||||
ret, ok := netip.AddrFromSlice(std)
|
||||
return ret.Unmap(), ok
|
||||
}
|
||||
|
||||
// MustFromStdIP is like FromStdIP, but it panics if std is invalid.
|
||||
func MustFromStdIP(std net.IP) netip.Addr {
|
||||
ret, ok := netip.AddrFromSlice(std)
|
||||
if !ok {
|
||||
panic("not a valid IP address")
|
||||
}
|
||||
return ret.Unmap()
|
||||
}
|
||||
|
||||
// FromStdIPRaw returns an IP from the standard library's IP type.
|
||||
// If std is invalid, ok is false.
|
||||
// Unlike FromStdIP, FromStdIPRaw does not do an implicit Unmap if
|
||||
// len(std) == 16 and contains an IPv6-mapped IPv4 address.
|
||||
//
|
||||
// Deprecated: use netip.AddrFromSlice instead.
|
||||
func FromStdIPRaw(std net.IP) (ip netip.Addr, ok bool) {
|
||||
return netip.AddrFromSlice(std)
|
||||
}
|
||||
|
||||
// AddrNext returns the IP following ip.
|
||||
// If there is none, it returns the IP zero value.
|
||||
//
|
||||
// Deprecated: use netip.Addr.Next instead.
|
||||
func AddrNext(ip netip.Addr) netip.Addr {
|
||||
addr := u128From16(ip.As16()).addOne()
|
||||
if ip.Is4() {
|
||||
if uint32(addr.lo) == 0 {
|
||||
// Overflowed.
|
||||
return netip.Addr{}
|
||||
}
|
||||
return addr.IP4()
|
||||
} else {
|
||||
if addr.isZero() {
|
||||
// Overflowed
|
||||
return netip.Addr{}
|
||||
}
|
||||
return addr.IP6().WithZone(ip.Zone())
|
||||
}
|
||||
}
|
||||
|
||||
// AddrPrior returns the IP before ip.
|
||||
// If there is none, it returns the IP zero value.
|
||||
//
|
||||
// Deprecated: use netip.Addr.Prev instead.
|
||||
func AddrPrior(ip netip.Addr) netip.Addr {
|
||||
addr := u128From16(ip.As16())
|
||||
if ip.Is4() {
|
||||
if uint32(addr.lo) == 0 {
|
||||
return netip.Addr{}
|
||||
}
|
||||
return addr.subOne().IP4()
|
||||
} else {
|
||||
if addr.isZero() {
|
||||
return netip.Addr{}
|
||||
}
|
||||
return addr.subOne().IP6().WithZone(ip.Zone())
|
||||
}
|
||||
}
|
||||
|
||||
// FromStdAddr maps the components of a standard library TCPAddr or
|
||||
// UDPAddr into an IPPort.
|
||||
func FromStdAddr(stdIP net.IP, port int, zone string) (_ netip.AddrPort, ok bool) {
|
||||
ip, ok := FromStdIP(stdIP)
|
||||
if !ok || port < 0 || port > math.MaxUint16 {
|
||||
return netip.AddrPort{}, false
|
||||
}
|
||||
ip = ip.Unmap()
|
||||
if zone != "" {
|
||||
if ip.Is4() {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
ip = ip.WithZone(zone)
|
||||
}
|
||||
return netip.AddrPortFrom(ip, uint16(port)), true
|
||||
}
|
||||
|
||||
// FromStdIPNet returns an netip.Prefix from the standard library's IPNet type.
|
||||
// If std is invalid, ok is false.
|
||||
func FromStdIPNet(std *net.IPNet) (prefix netip.Prefix, ok bool) {
|
||||
ip, ok := FromStdIP(std.IP)
|
||||
if !ok {
|
||||
return netip.Prefix{}, false
|
||||
}
|
||||
|
||||
if l := len(std.Mask); l != net.IPv4len && l != net.IPv6len {
|
||||
// Invalid mask.
|
||||
return netip.Prefix{}, false
|
||||
}
|
||||
|
||||
ones, bits := std.Mask.Size()
|
||||
if ones == 0 && bits == 0 {
|
||||
// IPPrefix does not support non-contiguous masks.
|
||||
return netip.Prefix{}, false
|
||||
}
|
||||
|
||||
return netip.PrefixFrom(ip, ones), true
|
||||
}
|
||||
|
||||
// RangeOfPrefix returns the inclusive range of IPs that p covers.
|
||||
//
|
||||
// If p is zero or otherwise invalid, Range returns the zero value.
|
||||
func RangeOfPrefix(p netip.Prefix) IPRange {
|
||||
p = p.Masked()
|
||||
if !p.IsValid() {
|
||||
return IPRange{}
|
||||
}
|
||||
return IPRangeFrom(p.Addr(), PrefixLastIP(p))
|
||||
}
|
||||
|
||||
// PrefixIPNet returns the net.IPNet representation of an netip.Prefix.
|
||||
// The returned value is always non-nil.
|
||||
// Any zone identifier is dropped in the conversion.
|
||||
func PrefixIPNet(p netip.Prefix) *net.IPNet {
|
||||
if !p.IsValid() {
|
||||
return &net.IPNet{}
|
||||
}
|
||||
return &net.IPNet{
|
||||
IP: p.Addr().AsSlice(),
|
||||
Mask: net.CIDRMask(p.Bits(), p.Addr().BitLen()),
|
||||
}
|
||||
}
|
||||
|
||||
// AddrIPNet returns the net.IPNet representation of an netip.Addr
|
||||
// with a mask corresponding to the addresses's bit length.
|
||||
// The returned value is always non-nil.
|
||||
// Any zone identifier is dropped in the conversion.
|
||||
func AddrIPNet(addr netip.Addr) *net.IPNet {
|
||||
if !addr.IsValid() {
|
||||
return &net.IPNet{}
|
||||
}
|
||||
return &net.IPNet{
|
||||
IP: addr.AsSlice(),
|
||||
Mask: net.CIDRMask(addr.BitLen(), addr.BitLen()),
|
||||
}
|
||||
}
|
||||
|
||||
// PrefixLastIP returns the last IP in the prefix.
|
||||
func PrefixLastIP(p netip.Prefix) netip.Addr {
|
||||
if !p.IsValid() {
|
||||
return netip.Addr{}
|
||||
}
|
||||
a16 := p.Addr().As16()
|
||||
var off uint8
|
||||
var bits uint8 = 128
|
||||
if p.Addr().Is4() {
|
||||
off = 12
|
||||
bits = 32
|
||||
}
|
||||
for b := uint8(p.Bits()); b < bits; b++ {
|
||||
byteNum, bitInByte := b/8, 7-(b%8)
|
||||
a16[off+byteNum] |= 1 << uint(bitInByte)
|
||||
}
|
||||
if p.Addr().Is4() {
|
||||
return netip.AddrFrom16(a16).Unmap()
|
||||
} else {
|
||||
return netip.AddrFrom16(a16) // doesn't unmap
|
||||
}
|
||||
}
|
||||
|
||||
// IPRange represents an inclusive range of IP addresses
|
||||
// from the same address family.
|
||||
//
|
||||
// The From and To IPs are inclusive bounds, with both included in the
|
||||
// range.
|
||||
//
|
||||
// To be valid, the From and To values must be non-zero, have matching
|
||||
// address families (IPv4 vs IPv6), and From must be less than or equal to To.
|
||||
// IPv6 zones are stripped out and ignored.
|
||||
// An invalid range may be ignored.
|
||||
type IPRange struct {
|
||||
// from is the initial IP address in the range.
|
||||
from netip.Addr
|
||||
|
||||
// to is the final IP address in the range.
|
||||
to netip.Addr
|
||||
}
|
||||
|
||||
// IPRangeFrom returns an IPRange from from to to.
|
||||
// It does not allocate.
|
||||
func IPRangeFrom(from, to netip.Addr) IPRange {
|
||||
return IPRange{
|
||||
from: from.WithZone(""),
|
||||
to: to.WithZone(""),
|
||||
}
|
||||
}
|
||||
|
||||
// From returns the lower bound of r.
|
||||
func (r IPRange) From() netip.Addr { return r.from }
|
||||
|
||||
// To returns the upper bound of r.
|
||||
func (r IPRange) To() netip.Addr { return r.to }
|
||||
|
||||
// ParseIPRange parses a range out of two IPs separated by a hyphen.
|
||||
//
|
||||
// It returns an error if the range is not valid.
|
||||
func ParseIPRange(s string) (IPRange, error) {
|
||||
var r IPRange
|
||||
h := strings.IndexByte(s, '-')
|
||||
if h == -1 {
|
||||
return r, fmt.Errorf("no hyphen in range %q", s)
|
||||
}
|
||||
from, to := s[:h], s[h+1:]
|
||||
var err error
|
||||
r.from, err = netip.ParseAddr(from)
|
||||
if err != nil {
|
||||
return r, fmt.Errorf("invalid From IP %q in range %q", from, s)
|
||||
}
|
||||
r.from = r.from.WithZone("")
|
||||
r.to, err = netip.ParseAddr(to)
|
||||
if err != nil {
|
||||
return r, fmt.Errorf("invalid To IP %q in range %q", to, s)
|
||||
}
|
||||
r.to = r.to.WithZone("")
|
||||
if !r.IsValid() {
|
||||
return r, fmt.Errorf("range %v to %v not valid", r.from, r.to)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// MustParseIPRange calls ParseIPRange(s) and panics on error.
|
||||
// It is intended for use in tests with hard-coded strings.
|
||||
func MustParseIPRange(s string) IPRange {
|
||||
r, err := ParseIPRange(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// String returns a string representation of the range.
|
||||
//
|
||||
// For a valid range, the form is "From-To" with a single hyphen
|
||||
// separating the IPs, the same format recognized by
|
||||
// ParseIPRange.
|
||||
func (r IPRange) String() string {
|
||||
if r.IsValid() {
|
||||
return fmt.Sprintf("%s-%s", r.from, r.to)
|
||||
}
|
||||
if !r.from.IsValid() || !r.to.IsValid() {
|
||||
return "zero IPRange"
|
||||
}
|
||||
return "invalid IPRange"
|
||||
}
|
||||
|
||||
// AppendTo appends a text encoding of r,
|
||||
// as generated by MarshalText,
|
||||
// to b and returns the extended buffer.
|
||||
func (r IPRange) AppendTo(b []byte) []byte {
|
||||
if r.IsZero() {
|
||||
return b
|
||||
}
|
||||
b = r.from.AppendTo(b)
|
||||
b = append(b, '-')
|
||||
b = r.to.AppendTo(b)
|
||||
return b
|
||||
}
|
||||
|
||||
// MarshalText implements the encoding.TextMarshaler interface,
|
||||
// The encoding is the same as returned by String, with one exception:
|
||||
// If ip is the zero value, the encoding is the empty string.
|
||||
func (r IPRange) MarshalText() ([]byte, error) {
|
||||
if r.IsZero() {
|
||||
return []byte(""), nil
|
||||
}
|
||||
var max int
|
||||
if r.from.Is4() {
|
||||
max = len("255.255.255.255-255.255.255.255")
|
||||
} else {
|
||||
max = len("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")
|
||||
}
|
||||
b := make([]byte, 0, max)
|
||||
return r.AppendTo(b), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface.
|
||||
// The IP range is expected in a form accepted by ParseIPRange.
|
||||
// It returns an error if *r is not the IPRange zero value.
|
||||
func (r *IPRange) UnmarshalText(text []byte) error {
|
||||
if *r != (IPRange{}) {
|
||||
return errors.New("refusing to Unmarshal into non-zero IPRange")
|
||||
}
|
||||
if len(text) == 0 {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
*r, err = ParseIPRange(string(text))
|
||||
return err
|
||||
}
|
||||
|
||||
// IsZero reports whether r is the zero value of the IPRange type.
|
||||
func (r IPRange) IsZero() bool {
|
||||
return r == IPRange{}
|
||||
}
|
||||
|
||||
// IsValid reports whether r.From() and r.To() are both non-zero and
|
||||
// obey the documented requirements: address families match, and From
|
||||
// is less than or equal to To.
|
||||
func (r IPRange) IsValid() bool {
|
||||
return r.from.IsValid() &&
|
||||
r.from.BitLen() == r.to.BitLen() &&
|
||||
r.from.Zone() == r.to.Zone() &&
|
||||
!r.to.Less(r.from)
|
||||
}
|
||||
|
||||
// Valid reports whether r.From() and r.To() are both non-zero and
|
||||
// obey the documented requirements: address families match, and From
|
||||
// is less than or equal to To.
|
||||
//
|
||||
// Deprecated: use the correctly named and identical IsValid method instead.
|
||||
func (r IPRange) Valid() bool { return r.IsValid() }
|
||||
|
||||
// Contains reports whether the range r includes addr.
|
||||
//
|
||||
// An invalid range always reports false.
|
||||
//
|
||||
// If ip has an IPv6 zone, Contains returns false,
|
||||
// because IPPrefixes strip zones.
|
||||
func (r IPRange) Contains(addr netip.Addr) bool {
|
||||
return r.IsValid() && addr.Zone() == "" && r.contains(addr)
|
||||
}
|
||||
|
||||
// contains is like Contains, but without the validity check.
|
||||
// addr must not have a zone.
|
||||
func (r IPRange) contains(addr netip.Addr) bool {
|
||||
return r.from.Compare(addr) <= 0 && r.to.Compare(addr) >= 0
|
||||
}
|
||||
|
||||
// less reports whether r is "before" other. It is before if r.From()
|
||||
// is before other.From(). If they're equal, then the larger range
|
||||
// (higher To()) comes first.
|
||||
func (r IPRange) less(other IPRange) bool {
|
||||
if cmp := r.from.Compare(other.from); cmp != 0 {
|
||||
return cmp < 0
|
||||
}
|
||||
return other.to.Less(r.to)
|
||||
}
|
||||
|
||||
// entirelyBefore returns whether r lies entirely before other in IP
|
||||
// space.
|
||||
func (r IPRange) entirelyBefore(other IPRange) bool {
|
||||
return r.to.Less(other.from)
|
||||
}
|
||||
|
||||
func lessOrEq(ip, ip2 netip.Addr) bool { return ip.Compare(ip2) <= 0 }
|
||||
|
||||
// entirelyWithin returns whether r is entirely contained within
|
||||
// other.
|
||||
func (r IPRange) coveredBy(other IPRange) bool {
|
||||
return lessOrEq(other.from, r.from) && lessOrEq(r.to, other.to)
|
||||
}
|
||||
|
||||
// inMiddleOf returns whether r is inside other, but not touching the
|
||||
// edges of other.
|
||||
func (r IPRange) inMiddleOf(other IPRange) bool {
|
||||
return other.from.Less(r.from) && r.to.Less(other.to)
|
||||
}
|
||||
|
||||
// overlapsStartOf returns whether r entirely overlaps the start of
|
||||
// other, but not all of other.
|
||||
func (r IPRange) overlapsStartOf(other IPRange) bool {
|
||||
return lessOrEq(r.from, other.from) && r.to.Less(other.to)
|
||||
}
|
||||
|
||||
// overlapsEndOf returns whether r entirely overlaps the end of
|
||||
// other, but not all of other.
|
||||
func (r IPRange) overlapsEndOf(other IPRange) bool {
|
||||
return other.from.Less(r.from) && lessOrEq(other.to, r.to)
|
||||
}
|
||||
|
||||
// mergeIPRanges returns the minimum and sorted set of IP ranges that
|
||||
// cover r.
|
||||
func mergeIPRanges(rr []IPRange) (out []IPRange, valid bool) {
|
||||
// Always return a copy of r, to avoid aliasing slice memory in
|
||||
// the caller.
|
||||
switch len(rr) {
|
||||
case 0:
|
||||
return nil, true
|
||||
case 1:
|
||||
return []IPRange{rr[0]}, true
|
||||
}
|
||||
|
||||
sort.Slice(rr, func(i, j int) bool { return rr[i].less(rr[j]) })
|
||||
out = make([]IPRange, 1, len(rr))
|
||||
out[0] = rr[0]
|
||||
for _, r := range rr[1:] {
|
||||
prev := &out[len(out)-1]
|
||||
switch {
|
||||
case !r.IsValid():
|
||||
// Invalid ranges make no sense to merge, refuse to
|
||||
// perform.
|
||||
return nil, false
|
||||
case prev.to.Next() == r.from:
|
||||
// prev and r touch, merge them.
|
||||
//
|
||||
// prev r
|
||||
// f------tf-----t
|
||||
prev.to = r.to
|
||||
case prev.to.Less(r.from):
|
||||
// No overlap and not adjacent (per previous case), no
|
||||
// merging possible.
|
||||
//
|
||||
// prev r
|
||||
// f------t f-----t
|
||||
out = append(out, r)
|
||||
case prev.to.Less(r.to):
|
||||
// Partial overlap, update prev
|
||||
//
|
||||
// prev
|
||||
// f------t
|
||||
// f-----t
|
||||
// r
|
||||
prev.to = r.to
|
||||
default:
|
||||
// r entirely contained in prev, nothing to do.
|
||||
//
|
||||
// prev
|
||||
// f--------t
|
||||
// f-----t
|
||||
// r
|
||||
}
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
// Overlaps reports whether p and o overlap at all.
|
||||
//
|
||||
// If p and o are of different address families or either are invalid,
|
||||
// it reports false.
|
||||
func (r IPRange) Overlaps(o IPRange) bool {
|
||||
return r.IsValid() &&
|
||||
o.IsValid() &&
|
||||
r.from.Compare(o.to) <= 0 &&
|
||||
o.from.Compare(r.to) <= 0
|
||||
}
|
||||
|
||||
// prefixMaker returns a address-family-corrected IPPrefix from a and bits,
|
||||
// where the input bits is always in the IPv6-mapped form for IPv4 addresses.
|
||||
type prefixMaker func(a uint128, bits uint8) netip.Prefix
|
||||
|
||||
// Prefixes returns the set of IPPrefix entries that covers r.
|
||||
//
|
||||
// If either of r's bounds are invalid, in the wrong order, or if
|
||||
// they're of different address families, then Prefixes returns nil.
|
||||
//
|
||||
// Prefixes necessarily allocates. See AppendPrefixes for a version that uses
|
||||
// memory you provide.
|
||||
func (r IPRange) Prefixes() []netip.Prefix {
|
||||
return r.AppendPrefixes(nil)
|
||||
}
|
||||
|
||||
// AppendPrefixes is an append version of IPRange.Prefixes. It appends
|
||||
// the netip.Prefix entries that cover r to dst.
|
||||
func (r IPRange) AppendPrefixes(dst []netip.Prefix) []netip.Prefix {
|
||||
if !r.IsValid() {
|
||||
return nil
|
||||
}
|
||||
return appendRangePrefixes(dst, r.prefixFrom128AndBits, u128From16(r.from.As16()), u128From16(r.to.As16()))
|
||||
}
|
||||
|
||||
func (r IPRange) prefixFrom128AndBits(a uint128, bits uint8) netip.Prefix {
|
||||
var ip netip.Addr
|
||||
if r.from.Is4() {
|
||||
bits -= 12 * 8
|
||||
ip = a.IP4()
|
||||
} else {
|
||||
ip = a.IP6()
|
||||
}
|
||||
return netip.PrefixFrom(ip, int(bits))
|
||||
}
|
||||
|
||||
// aZeroBSet is whether, after the common bits, a is all zero bits and
|
||||
// b is all set (one) bits.
|
||||
func comparePrefixes(a, b uint128) (common uint8, aZeroBSet bool) {
|
||||
common = a.commonPrefixLen(b)
|
||||
|
||||
// See whether a and b, after their common shared bits, end
|
||||
// in all zero bits or all one bits, respectively.
|
||||
if common == 128 {
|
||||
return common, true
|
||||
}
|
||||
|
||||
m := mask6[common]
|
||||
return common, (a.xor(a.and(m)).isZero() &&
|
||||
b.or(m) == uint128{^uint64(0), ^uint64(0)})
|
||||
}
|
||||
|
||||
// Prefix returns r as an IPPrefix, if it can be presented exactly as such.
|
||||
// If r is not valid or is not exactly equal to one prefix, ok is false.
|
||||
func (r IPRange) Prefix() (p netip.Prefix, ok bool) {
|
||||
if !r.IsValid() {
|
||||
return
|
||||
}
|
||||
from128 := u128From16(r.from.As16())
|
||||
to128 := u128From16(r.to.As16())
|
||||
if common, ok := comparePrefixes(from128, to128); ok {
|
||||
return r.prefixFrom128AndBits(from128, common), true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func appendRangePrefixes(dst []netip.Prefix, makePrefix prefixMaker, a, b uint128) []netip.Prefix {
|
||||
common, ok := comparePrefixes(a, b)
|
||||
if ok {
|
||||
// a to b represents a whole range, like 10.50.0.0/16.
|
||||
// (a being 10.50.0.0 and b being 10.50.255.255)
|
||||
return append(dst, makePrefix(a, common))
|
||||
}
|
||||
// Otherwise recursively do both halves.
|
||||
dst = appendRangePrefixes(dst, makePrefix, a, a.bitsSetFrom(common+1))
|
||||
dst = appendRangePrefixes(dst, makePrefix, b.bitsClearedFrom(common+1), b)
|
||||
return dst
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
// Copyright 2020 The Inet.Af AUTHORS. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package netipx
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math/bits"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
// uint128 represents a uint128 using two uint64s.
|
||||
//
|
||||
// When the methods below mention a bit number, bit 0 is the most
|
||||
// significant bit (in hi) and bit 127 is the lowest (lo&1).
|
||||
type uint128 struct {
|
||||
hi uint64
|
||||
lo uint64
|
||||
}
|
||||
|
||||
func u128From16(a [16]byte) uint128 {
|
||||
return uint128{
|
||||
binary.BigEndian.Uint64(a[:8]),
|
||||
binary.BigEndian.Uint64(a[8:]),
|
||||
}
|
||||
}
|
||||
|
||||
func (u uint128) IP6() netip.Addr {
|
||||
var a [16]byte
|
||||
binary.BigEndian.PutUint64(a[:8], u.hi)
|
||||
binary.BigEndian.PutUint64(a[8:], u.lo)
|
||||
return netip.AddrFrom16(a)
|
||||
}
|
||||
|
||||
func (u uint128) IP4() netip.Addr {
|
||||
var a [8]byte
|
||||
binary.BigEndian.PutUint64(a[:], u.lo)
|
||||
return netip.AddrFrom4([4]byte{a[4], a[5], a[6], a[7]})
|
||||
}
|
||||
|
||||
// isZero reports whether u == 0.
|
||||
//
|
||||
// It's faster than u == (uint128{}) because the compiler (as of Go
|
||||
// 1.15/1.16b1) doesn't do this trick and instead inserts a branch in
|
||||
// its eq alg's generated code.
|
||||
func (u uint128) isZero() bool { return u.hi|u.lo == 0 }
|
||||
|
||||
// and returns the bitwise AND of u and m (u&m).
|
||||
func (u uint128) and(m uint128) uint128 {
|
||||
return uint128{u.hi & m.hi, u.lo & m.lo}
|
||||
}
|
||||
|
||||
// xor returns the bitwise XOR of u and m (u^m).
|
||||
func (u uint128) xor(m uint128) uint128 {
|
||||
return uint128{u.hi ^ m.hi, u.lo ^ m.lo}
|
||||
}
|
||||
|
||||
// or returns the bitwise OR of u and m (u|m).
|
||||
func (u uint128) or(m uint128) uint128 {
|
||||
return uint128{u.hi | m.hi, u.lo | m.lo}
|
||||
}
|
||||
|
||||
// not returns the bitwise NOT of u.
|
||||
func (u uint128) not() uint128 {
|
||||
return uint128{^u.hi, ^u.lo}
|
||||
}
|
||||
|
||||
// subOne returns u - 1.
|
||||
func (u uint128) subOne() uint128 {
|
||||
lo, borrow := bits.Sub64(u.lo, 1, 0)
|
||||
return uint128{u.hi - borrow, lo}
|
||||
}
|
||||
|
||||
// addOne returns u + 1.
|
||||
func (u uint128) addOne() uint128 {
|
||||
lo, carry := bits.Add64(u.lo, 1, 0)
|
||||
return uint128{u.hi + carry, lo}
|
||||
}
|
||||
|
||||
func u64CommonPrefixLen(a, b uint64) uint8 {
|
||||
return uint8(bits.LeadingZeros64(a ^ b))
|
||||
}
|
||||
|
||||
func (u uint128) commonPrefixLen(v uint128) (n uint8) {
|
||||
if n = u64CommonPrefixLen(u.hi, v.hi); n == 64 {
|
||||
n += u64CommonPrefixLen(u.lo, v.lo)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// func (u *uint128) halves() [2]*uint64 {
|
||||
// return [2]*uint64{&u.hi, &u.lo}
|
||||
// }
|
||||
|
||||
// bitsSetFrom returns a copy of u with the given bit
|
||||
// and all subsequent ones set.
|
||||
func (u uint128) bitsSetFrom(bit uint8) uint128 {
|
||||
return u.or(mask6[bit].not())
|
||||
}
|
||||
|
||||
// bitsClearedFrom returns a copy of u with the given bit
|
||||
// and all subsequent ones cleared.
|
||||
func (u uint128) bitsClearedFrom(bit uint8) uint128 {
|
||||
return u.and(mask6[bit])
|
||||
}
|
|
@ -50,7 +50,7 @@ func (ih InvalidHashPrefixError) Error() string {
|
|||
type InvalidCostError int
|
||||
|
||||
func (ic InvalidCostError) Error() string {
|
||||
return fmt.Sprintf("crypto/bcrypt: cost %d is outside allowed range (%d,%d)", int(ic), int(MinCost), int(MaxCost))
|
||||
return fmt.Sprintf("crypto/bcrypt: cost %d is outside allowed range (%d,%d)", int(ic), MinCost, MaxCost)
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
|
@ -251,7 +251,7 @@ type algorithmOpenSSHCertSigner struct {
|
|||
// private key is held by signer. It returns an error if the public key in cert
|
||||
// doesn't match the key used by signer.
|
||||
func NewCertSigner(cert *Certificate, signer Signer) (Signer, error) {
|
||||
if bytes.Compare(cert.Key.Marshal(), signer.PublicKey().Marshal()) != 0 {
|
||||
if !bytes.Equal(cert.Key.Marshal(), signer.PublicKey().Marshal()) {
|
||||
return nil, errors.New("ssh: signer and cert have different public key")
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ import (
|
|||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"golang.org/x/crypto/chacha20"
|
||||
"golang.org/x/crypto/internal/poly1305"
|
||||
|
@ -97,13 +96,13 @@ func streamCipherMode(skip int, createFunc func(key, iv []byte) (cipher.Stream,
|
|||
// are not supported and will not be negotiated, even if explicitly requested in
|
||||
// ClientConfig.Crypto.Ciphers.
|
||||
var cipherModes = map[string]*cipherMode{
|
||||
// Ciphers from RFC4344, which introduced many CTR-based ciphers. Algorithms
|
||||
// Ciphers from RFC 4344, which introduced many CTR-based ciphers. Algorithms
|
||||
// are defined in the order specified in the RFC.
|
||||
"aes128-ctr": {16, aes.BlockSize, streamCipherMode(0, newAESCTR)},
|
||||
"aes192-ctr": {24, aes.BlockSize, streamCipherMode(0, newAESCTR)},
|
||||
"aes256-ctr": {32, aes.BlockSize, streamCipherMode(0, newAESCTR)},
|
||||
|
||||
// Ciphers from RFC4345, which introduces security-improved arcfour ciphers.
|
||||
// Ciphers from RFC 4345, which introduces security-improved arcfour ciphers.
|
||||
// They are defined in the order specified in the RFC.
|
||||
"arcfour128": {16, 0, streamCipherMode(1536, newRC4)},
|
||||
"arcfour256": {32, 0, streamCipherMode(1536, newRC4)},
|
||||
|
@ -111,7 +110,7 @@ var cipherModes = map[string]*cipherMode{
|
|||
// Cipher defined in RFC 4253, which describes SSH Transport Layer Protocol.
|
||||
// Note that this cipher is not safe, as stated in RFC 4253: "Arcfour (and
|
||||
// RC4) has problems with weak keys, and should be used with caution."
|
||||
// RFC4345 introduces improved versions of Arcfour.
|
||||
// RFC 4345 introduces improved versions of Arcfour.
|
||||
"arcfour": {16, 0, streamCipherMode(0, newRC4)},
|
||||
|
||||
// AEAD ciphers
|
||||
|
@ -497,7 +496,7 @@ func (c *cbcCipher) readCipherPacket(seqNum uint32, r io.Reader) ([]byte, error)
|
|||
// data, to make distinguishing between
|
||||
// failing MAC and failing length check more
|
||||
// difficult.
|
||||
io.CopyN(ioutil.Discard, r, int64(c.oracleCamouflage))
|
||||
io.CopyN(io.Discard, r, int64(c.oracleCamouflage))
|
||||
}
|
||||
}
|
||||
return p, err
|
||||
|
@ -642,7 +641,7 @@ const chacha20Poly1305ID = "chacha20-poly1305@openssh.com"
|
|||
//
|
||||
// https://tools.ietf.org/html/draft-josefsson-ssh-chacha20-poly1305-openssh-00
|
||||
//
|
||||
// the methods here also implement padding, which RFC4253 Section 6
|
||||
// the methods here also implement padding, which RFC 4253 Section 6
|
||||
// also requires of stream ciphers.
|
||||
type chacha20Poly1305Cipher struct {
|
||||
lengthKey [32]byte
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
_ "crypto/sha1"
|
||||
|
@ -118,6 +119,20 @@ func algorithmsForKeyFormat(keyFormat string) []string {
|
|||
}
|
||||
}
|
||||
|
||||
// supportedPubKeyAuthAlgos specifies the supported client public key
|
||||
// authentication algorithms. Note that this doesn't include certificate types
|
||||
// since those use the underlying algorithm. This list is sent to the client if
|
||||
// it supports the server-sig-algs extension. Order is irrelevant.
|
||||
var supportedPubKeyAuthAlgos = []string{
|
||||
KeyAlgoED25519,
|
||||
KeyAlgoSKED25519, KeyAlgoSKECDSA256,
|
||||
KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521,
|
||||
KeyAlgoRSASHA256, KeyAlgoRSASHA512, KeyAlgoRSA,
|
||||
KeyAlgoDSA,
|
||||
}
|
||||
|
||||
var supportedPubKeyAuthAlgosList = strings.Join(supportedPubKeyAuthAlgos, ",")
|
||||
|
||||
// unexpectedMessageError results when the SSH message that we received didn't
|
||||
// match what we wanted.
|
||||
func unexpectedMessageError(expected, got uint8) error {
|
||||
|
@ -149,7 +164,7 @@ type directionAlgorithms struct {
|
|||
|
||||
// rekeyBytes returns a rekeying intervals in bytes.
|
||||
func (a *directionAlgorithms) rekeyBytes() int64 {
|
||||
// According to RFC4344 block ciphers should rekey after
|
||||
// According to RFC 4344 block ciphers should rekey after
|
||||
// 2^(BLOCKSIZE/4) blocks. For all AES flavors BLOCKSIZE is
|
||||
// 128.
|
||||
switch a.Cipher {
|
||||
|
@ -158,7 +173,7 @@ func (a *directionAlgorithms) rekeyBytes() int64 {
|
|||
|
||||
}
|
||||
|
||||
// For others, stick with RFC4253 recommendation to rekey after 1 Gb of data.
|
||||
// For others, stick with RFC 4253 recommendation to rekey after 1 Gb of data.
|
||||
return 1 << 30
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ type Conn interface {
|
|||
|
||||
// SendRequest sends a global request, and returns the
|
||||
// reply. If wantReply is true, it returns the response status
|
||||
// and payload. See also RFC4254, section 4.
|
||||
// and payload. See also RFC 4254, section 4.
|
||||
SendRequest(name string, wantReply bool, payload []byte) (bool, []byte, error)
|
||||
|
||||
// OpenChannel tries to open an channel. If the request is
|
||||
|
|
|
@ -615,7 +615,8 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if t.sessionID == nil {
|
||||
firstKeyExchange := t.sessionID == nil
|
||||
if firstKeyExchange {
|
||||
t.sessionID = result.H
|
||||
}
|
||||
result.SessionID = t.sessionID
|
||||
|
@ -626,6 +627,24 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error {
|
|||
if err = t.conn.writePacket([]byte{msgNewKeys}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// On the server side, after the first SSH_MSG_NEWKEYS, send a SSH_MSG_EXT_INFO
|
||||
// message with the server-sig-algs extension if the client supports it. See
|
||||
// RFC 8308, Sections 2.4 and 3.1.
|
||||
if !isClient && firstKeyExchange && contains(clientInit.KexAlgos, "ext-info-c") {
|
||||
extInfo := &extInfoMsg{
|
||||
NumExtensions: 1,
|
||||
Payload: make([]byte, 0, 4+15+4+len(supportedPubKeyAuthAlgosList)),
|
||||
}
|
||||
extInfo.Payload = appendInt(extInfo.Payload, len("server-sig-algs"))
|
||||
extInfo.Payload = append(extInfo.Payload, "server-sig-algs"...)
|
||||
extInfo.Payload = appendInt(extInfo.Payload, len(supportedPubKeyAuthAlgosList))
|
||||
extInfo.Payload = append(extInfo.Payload, supportedPubKeyAuthAlgosList...)
|
||||
if err := t.conn.writePacket(Marshal(extInfo)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if packet, err := t.conn.readPacket(); err != nil {
|
||||
return err
|
||||
} else if packet[0] != msgNewKeys {
|
||||
|
|
|
@ -184,7 +184,7 @@ func ParseKnownHosts(in []byte) (marker string, hosts []string, pubKey PublicKey
|
|||
return "", nil, nil, "", nil, io.EOF
|
||||
}
|
||||
|
||||
// ParseAuthorizedKeys parses a public key from an authorized_keys
|
||||
// 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) {
|
||||
for len(in) > 0 {
|
||||
|
|
|
@ -68,7 +68,7 @@ type kexInitMsg struct {
|
|||
|
||||
// See RFC 4253, section 8.
|
||||
|
||||
// Diffie-Helman
|
||||
// Diffie-Hellman
|
||||
const msgKexDHInit = 30
|
||||
|
||||
type kexDHInitMsg struct {
|
||||
|
|
|
@ -68,8 +68,16 @@ type ServerConfig struct {
|
|||
|
||||
// NoClientAuth is true if clients are allowed to connect without
|
||||
// authenticating.
|
||||
// To determine NoClientAuth at runtime, set NoClientAuth to true
|
||||
// and the optional NoClientAuthCallback to a non-nil value.
|
||||
NoClientAuth bool
|
||||
|
||||
// NoClientAuthCallback, if non-nil, is called when a user
|
||||
// attempts to authenticate with auth method "none".
|
||||
// NoClientAuth must also be set to true for this be used, or
|
||||
// this func is unused.
|
||||
NoClientAuthCallback func(ConnMetadata) (*Permissions, error)
|
||||
|
||||
// MaxAuthTries specifies the maximum number of authentication attempts
|
||||
// permitted per connection. If set to a negative number, the number of
|
||||
// attempts are unlimited. If set to zero, the number of attempts are limited
|
||||
|
@ -283,15 +291,6 @@ func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error)
|
|||
return perms, err
|
||||
}
|
||||
|
||||
func isAcceptableAlgo(algo string) bool {
|
||||
switch algo {
|
||||
case KeyAlgoRSA, KeyAlgoRSASHA256, KeyAlgoRSASHA512, KeyAlgoDSA, KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521, KeyAlgoSKECDSA256, KeyAlgoED25519, KeyAlgoSKED25519,
|
||||
CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01, CertAlgoSKECDSA256v01, CertAlgoED25519v01, CertAlgoSKED25519v01:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkSourceAddress(addr net.Addr, sourceAddrs string) error {
|
||||
if addr == nil {
|
||||
return errors.New("ssh: no address known for client, but source-address match required")
|
||||
|
@ -455,7 +454,11 @@ userAuthLoop:
|
|||
switch userAuthReq.Method {
|
||||
case "none":
|
||||
if config.NoClientAuth {
|
||||
authErr = nil
|
||||
if config.NoClientAuthCallback != nil {
|
||||
perms, authErr = config.NoClientAuthCallback(s)
|
||||
} else {
|
||||
authErr = nil
|
||||
}
|
||||
}
|
||||
|
||||
// allow initial attempt of 'none' without penalty
|
||||
|
@ -502,7 +505,7 @@ userAuthLoop:
|
|||
return nil, parseError(msgUserAuthRequest)
|
||||
}
|
||||
algo := string(algoBytes)
|
||||
if !isAcceptableAlgo(algo) {
|
||||
if !contains(supportedPubKeyAuthAlgos, underlyingAlgo(algo)) {
|
||||
authErr = fmt.Errorf("ssh: algorithm %q not accepted", algo)
|
||||
break
|
||||
}
|
||||
|
@ -560,7 +563,7 @@ userAuthLoop:
|
|||
// algorithm name that corresponds to algo with
|
||||
// sig.Format. This is usually the same, but
|
||||
// for certs, the names differ.
|
||||
if !isAcceptableAlgo(sig.Format) {
|
||||
if !contains(supportedPubKeyAuthAlgos, sig.Format) {
|
||||
authErr = fmt.Errorf("ssh: algorithm %q not accepted", sig.Format)
|
||||
break
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
)
|
||||
|
||||
|
@ -124,7 +123,7 @@ type Session struct {
|
|||
// output and error.
|
||||
//
|
||||
// If either is nil, Run connects the corresponding file
|
||||
// descriptor to an instance of ioutil.Discard. There is a
|
||||
// descriptor to an instance of io.Discard. There is a
|
||||
// fixed amount of buffering that is shared for the two streams.
|
||||
// If either blocks it may eventually cause the remote
|
||||
// command to block.
|
||||
|
@ -506,7 +505,7 @@ func (s *Session) stdout() {
|
|||
return
|
||||
}
|
||||
if s.Stdout == nil {
|
||||
s.Stdout = ioutil.Discard
|
||||
s.Stdout = io.Discard
|
||||
}
|
||||
s.copyFuncs = append(s.copyFuncs, func() error {
|
||||
_, err := io.Copy(s.Stdout, s.ch)
|
||||
|
@ -519,7 +518,7 @@ func (s *Session) stderr() {
|
|||
return
|
||||
}
|
||||
if s.Stderr == nil {
|
||||
s.Stderr = ioutil.Discard
|
||||
s.Stderr = io.Discard
|
||||
}
|
||||
s.copyFuncs = append(s.copyFuncs, func() error {
|
||||
_, err := io.Copy(s.Stderr, s.ch.Stderr())
|
||||
|
|
|
@ -27,7 +27,14 @@ func buildCommonHeaderMaps() {
|
|||
"accept-language",
|
||||
"accept-ranges",
|
||||
"age",
|
||||
"access-control-allow-credentials",
|
||||
"access-control-allow-headers",
|
||||
"access-control-allow-methods",
|
||||
"access-control-allow-origin",
|
||||
"access-control-expose-headers",
|
||||
"access-control-max-age",
|
||||
"access-control-request-headers",
|
||||
"access-control-request-method",
|
||||
"allow",
|
||||
"authorization",
|
||||
"cache-control",
|
||||
|
@ -53,6 +60,7 @@ func buildCommonHeaderMaps() {
|
|||
"link",
|
||||
"location",
|
||||
"max-forwards",
|
||||
"origin",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"range",
|
||||
|
@ -68,6 +76,8 @@ func buildCommonHeaderMaps() {
|
|||
"vary",
|
||||
"via",
|
||||
"www-authenticate",
|
||||
"x-forwarded-for",
|
||||
"x-forwarded-proto",
|
||||
}
|
||||
commonLowerHeader = make(map[string]string, len(common))
|
||||
commonCanonHeader = make(map[string]string, len(common))
|
||||
|
@ -85,3 +95,11 @@ func lowerHeader(v string) (lower string, ascii bool) {
|
|||
}
|
||||
return asciiToLower(v)
|
||||
}
|
||||
|
||||
func canonicalHeader(v string) string {
|
||||
buildCommonHeaderMapsOnce()
|
||||
if s, ok := commonCanonHeader[v]; ok {
|
||||
return s
|
||||
}
|
||||
return http.CanonicalHeaderKey(v)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
// go generate gen.go
|
||||
// Code generated by the command above; DO NOT EDIT.
|
||||
|
||||
package hpack
|
||||
|
||||
var staticTable = &headerFieldTable{
|
||||
evictCount: 0,
|
||||
byName: map[string]uint64{
|
||||
":authority": 1,
|
||||
":method": 3,
|
||||
":path": 5,
|
||||
":scheme": 7,
|
||||
":status": 14,
|
||||
"accept-charset": 15,
|
||||
"accept-encoding": 16,
|
||||
"accept-language": 17,
|
||||
"accept-ranges": 18,
|
||||
"accept": 19,
|
||||
"access-control-allow-origin": 20,
|
||||
"age": 21,
|
||||
"allow": 22,
|
||||
"authorization": 23,
|
||||
"cache-control": 24,
|
||||
"content-disposition": 25,
|
||||
"content-encoding": 26,
|
||||
"content-language": 27,
|
||||
"content-length": 28,
|
||||
"content-location": 29,
|
||||
"content-range": 30,
|
||||
"content-type": 31,
|
||||
"cookie": 32,
|
||||
"date": 33,
|
||||
"etag": 34,
|
||||
"expect": 35,
|
||||
"expires": 36,
|
||||
"from": 37,
|
||||
"host": 38,
|
||||
"if-match": 39,
|
||||
"if-modified-since": 40,
|
||||
"if-none-match": 41,
|
||||
"if-range": 42,
|
||||
"if-unmodified-since": 43,
|
||||
"last-modified": 44,
|
||||
"link": 45,
|
||||
"location": 46,
|
||||
"max-forwards": 47,
|
||||
"proxy-authenticate": 48,
|
||||
"proxy-authorization": 49,
|
||||
"range": 50,
|
||||
"referer": 51,
|
||||
"refresh": 52,
|
||||
"retry-after": 53,
|
||||
"server": 54,
|
||||
"set-cookie": 55,
|
||||
"strict-transport-security": 56,
|
||||
"transfer-encoding": 57,
|
||||
"user-agent": 58,
|
||||
"vary": 59,
|
||||
"via": 60,
|
||||
"www-authenticate": 61,
|
||||
},
|
||||
byNameValue: map[pairNameValue]uint64{
|
||||
{name: ":authority", value: ""}: 1,
|
||||
{name: ":method", value: "GET"}: 2,
|
||||
{name: ":method", value: "POST"}: 3,
|
||||
{name: ":path", value: "/"}: 4,
|
||||
{name: ":path", value: "/index.html"}: 5,
|
||||
{name: ":scheme", value: "http"}: 6,
|
||||
{name: ":scheme", value: "https"}: 7,
|
||||
{name: ":status", value: "200"}: 8,
|
||||
{name: ":status", value: "204"}: 9,
|
||||
{name: ":status", value: "206"}: 10,
|
||||
{name: ":status", value: "304"}: 11,
|
||||
{name: ":status", value: "400"}: 12,
|
||||
{name: ":status", value: "404"}: 13,
|
||||
{name: ":status", value: "500"}: 14,
|
||||
{name: "accept-charset", value: ""}: 15,
|
||||
{name: "accept-encoding", value: "gzip, deflate"}: 16,
|
||||
{name: "accept-language", value: ""}: 17,
|
||||
{name: "accept-ranges", value: ""}: 18,
|
||||
{name: "accept", value: ""}: 19,
|
||||
{name: "access-control-allow-origin", value: ""}: 20,
|
||||
{name: "age", value: ""}: 21,
|
||||
{name: "allow", value: ""}: 22,
|
||||
{name: "authorization", value: ""}: 23,
|
||||
{name: "cache-control", value: ""}: 24,
|
||||
{name: "content-disposition", value: ""}: 25,
|
||||
{name: "content-encoding", value: ""}: 26,
|
||||
{name: "content-language", value: ""}: 27,
|
||||
{name: "content-length", value: ""}: 28,
|
||||
{name: "content-location", value: ""}: 29,
|
||||
{name: "content-range", value: ""}: 30,
|
||||
{name: "content-type", value: ""}: 31,
|
||||
{name: "cookie", value: ""}: 32,
|
||||
{name: "date", value: ""}: 33,
|
||||
{name: "etag", value: ""}: 34,
|
||||
{name: "expect", value: ""}: 35,
|
||||
{name: "expires", value: ""}: 36,
|
||||
{name: "from", value: ""}: 37,
|
||||
{name: "host", value: ""}: 38,
|
||||
{name: "if-match", value: ""}: 39,
|
||||
{name: "if-modified-since", value: ""}: 40,
|
||||
{name: "if-none-match", value: ""}: 41,
|
||||
{name: "if-range", value: ""}: 42,
|
||||
{name: "if-unmodified-since", value: ""}: 43,
|
||||
{name: "last-modified", value: ""}: 44,
|
||||
{name: "link", value: ""}: 45,
|
||||
{name: "location", value: ""}: 46,
|
||||
{name: "max-forwards", value: ""}: 47,
|
||||
{name: "proxy-authenticate", value: ""}: 48,
|
||||
{name: "proxy-authorization", value: ""}: 49,
|
||||
{name: "range", value: ""}: 50,
|
||||
{name: "referer", value: ""}: 51,
|
||||
{name: "refresh", value: ""}: 52,
|
||||
{name: "retry-after", value: ""}: 53,
|
||||
{name: "server", value: ""}: 54,
|
||||
{name: "set-cookie", value: ""}: 55,
|
||||
{name: "strict-transport-security", value: ""}: 56,
|
||||
{name: "transfer-encoding", value: ""}: 57,
|
||||
{name: "user-agent", value: ""}: 58,
|
||||
{name: "vary", value: ""}: 59,
|
||||
{name: "via", value: ""}: 60,
|
||||
{name: "www-authenticate", value: ""}: 61,
|
||||
},
|
||||
ents: []HeaderField{
|
||||
{Name: ":authority", Value: "", Sensitive: false},
|
||||
{Name: ":method", Value: "GET", Sensitive: false},
|
||||
{Name: ":method", Value: "POST", Sensitive: false},
|
||||
{Name: ":path", Value: "/", Sensitive: false},
|
||||
{Name: ":path", Value: "/index.html", Sensitive: false},
|
||||
{Name: ":scheme", Value: "http", Sensitive: false},
|
||||
{Name: ":scheme", Value: "https", Sensitive: false},
|
||||
{Name: ":status", Value: "200", Sensitive: false},
|
||||
{Name: ":status", Value: "204", Sensitive: false},
|
||||
{Name: ":status", Value: "206", Sensitive: false},
|
||||
{Name: ":status", Value: "304", Sensitive: false},
|
||||
{Name: ":status", Value: "400", Sensitive: false},
|
||||
{Name: ":status", Value: "404", Sensitive: false},
|
||||
{Name: ":status", Value: "500", Sensitive: false},
|
||||
{Name: "accept-charset", Value: "", Sensitive: false},
|
||||
{Name: "accept-encoding", Value: "gzip, deflate", Sensitive: false},
|
||||
{Name: "accept-language", Value: "", Sensitive: false},
|
||||
{Name: "accept-ranges", Value: "", Sensitive: false},
|
||||
{Name: "accept", Value: "", Sensitive: false},
|
||||
{Name: "access-control-allow-origin", Value: "", Sensitive: false},
|
||||
{Name: "age", Value: "", Sensitive: false},
|
||||
{Name: "allow", Value: "", Sensitive: false},
|
||||
{Name: "authorization", Value: "", Sensitive: false},
|
||||
{Name: "cache-control", Value: "", Sensitive: false},
|
||||
{Name: "content-disposition", Value: "", Sensitive: false},
|
||||
{Name: "content-encoding", Value: "", Sensitive: false},
|
||||
{Name: "content-language", Value: "", Sensitive: false},
|
||||
{Name: "content-length", Value: "", Sensitive: false},
|
||||
{Name: "content-location", Value: "", Sensitive: false},
|
||||
{Name: "content-range", Value: "", Sensitive: false},
|
||||
{Name: "content-type", Value: "", Sensitive: false},
|
||||
{Name: "cookie", Value: "", Sensitive: false},
|
||||
{Name: "date", Value: "", Sensitive: false},
|
||||
{Name: "etag", Value: "", Sensitive: false},
|
||||
{Name: "expect", Value: "", Sensitive: false},
|
||||
{Name: "expires", Value: "", Sensitive: false},
|
||||
{Name: "from", Value: "", Sensitive: false},
|
||||
{Name: "host", Value: "", Sensitive: false},
|
||||
{Name: "if-match", Value: "", Sensitive: false},
|
||||
{Name: "if-modified-since", Value: "", Sensitive: false},
|
||||
{Name: "if-none-match", Value: "", Sensitive: false},
|
||||
{Name: "if-range", Value: "", Sensitive: false},
|
||||
{Name: "if-unmodified-since", Value: "", Sensitive: false},
|
||||
{Name: "last-modified", Value: "", Sensitive: false},
|
||||
{Name: "link", Value: "", Sensitive: false},
|
||||
{Name: "location", Value: "", Sensitive: false},
|
||||
{Name: "max-forwards", Value: "", Sensitive: false},
|
||||
{Name: "proxy-authenticate", Value: "", Sensitive: false},
|
||||
{Name: "proxy-authorization", Value: "", Sensitive: false},
|
||||
{Name: "range", Value: "", Sensitive: false},
|
||||
{Name: "referer", Value: "", Sensitive: false},
|
||||
{Name: "refresh", Value: "", Sensitive: false},
|
||||
{Name: "retry-after", Value: "", Sensitive: false},
|
||||
{Name: "server", Value: "", Sensitive: false},
|
||||
{Name: "set-cookie", Value: "", Sensitive: false},
|
||||
{Name: "strict-transport-security", Value: "", Sensitive: false},
|
||||
{Name: "transfer-encoding", Value: "", Sensitive: false},
|
||||
{Name: "user-agent", Value: "", Sensitive: false},
|
||||
{Name: "vary", Value: "", Sensitive: false},
|
||||
{Name: "via", Value: "", Sensitive: false},
|
||||
{Name: "www-authenticate", Value: "", Sensitive: false},
|
||||
},
|
||||
}
|
|
@ -96,8 +96,7 @@ func (t *headerFieldTable) evictOldest(n int) {
|
|||
// meaning t.ents is reversed for dynamic tables. Hence, when t is a dynamic
|
||||
// table, the return value i actually refers to the entry t.ents[t.len()-i].
|
||||
//
|
||||
// All tables are assumed to be a dynamic tables except for the global
|
||||
// staticTable pointer.
|
||||
// All tables are assumed to be a dynamic tables except for the global staticTable.
|
||||
//
|
||||
// See Section 2.3.3.
|
||||
func (t *headerFieldTable) search(f HeaderField) (i uint64, nameValueMatch bool) {
|
||||
|
@ -125,81 +124,6 @@ func (t *headerFieldTable) idToIndex(id uint64) uint64 {
|
|||
return k + 1
|
||||
}
|
||||
|
||||
// http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-07#appendix-B
|
||||
var staticTable = newStaticTable()
|
||||
var staticTableEntries = [...]HeaderField{
|
||||
{Name: ":authority"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: ":method", Value: "POST"},
|
||||
{Name: ":path", Value: "/"},
|
||||
{Name: ":path", Value: "/index.html"},
|
||||
{Name: ":scheme", Value: "http"},
|
||||
{Name: ":scheme", Value: "https"},
|
||||
{Name: ":status", Value: "200"},
|
||||
{Name: ":status", Value: "204"},
|
||||
{Name: ":status", Value: "206"},
|
||||
{Name: ":status", Value: "304"},
|
||||
{Name: ":status", Value: "400"},
|
||||
{Name: ":status", Value: "404"},
|
||||
{Name: ":status", Value: "500"},
|
||||
{Name: "accept-charset"},
|
||||
{Name: "accept-encoding", Value: "gzip, deflate"},
|
||||
{Name: "accept-language"},
|
||||
{Name: "accept-ranges"},
|
||||
{Name: "accept"},
|
||||
{Name: "access-control-allow-origin"},
|
||||
{Name: "age"},
|
||||
{Name: "allow"},
|
||||
{Name: "authorization"},
|
||||
{Name: "cache-control"},
|
||||
{Name: "content-disposition"},
|
||||
{Name: "content-encoding"},
|
||||
{Name: "content-language"},
|
||||
{Name: "content-length"},
|
||||
{Name: "content-location"},
|
||||
{Name: "content-range"},
|
||||
{Name: "content-type"},
|
||||
{Name: "cookie"},
|
||||
{Name: "date"},
|
||||
{Name: "etag"},
|
||||
{Name: "expect"},
|
||||
{Name: "expires"},
|
||||
{Name: "from"},
|
||||
{Name: "host"},
|
||||
{Name: "if-match"},
|
||||
{Name: "if-modified-since"},
|
||||
{Name: "if-none-match"},
|
||||
{Name: "if-range"},
|
||||
{Name: "if-unmodified-since"},
|
||||
{Name: "last-modified"},
|
||||
{Name: "link"},
|
||||
{Name: "location"},
|
||||
{Name: "max-forwards"},
|
||||
{Name: "proxy-authenticate"},
|
||||
{Name: "proxy-authorization"},
|
||||
{Name: "range"},
|
||||
{Name: "referer"},
|
||||
{Name: "refresh"},
|
||||
{Name: "retry-after"},
|
||||
{Name: "server"},
|
||||
{Name: "set-cookie"},
|
||||
{Name: "strict-transport-security"},
|
||||
{Name: "transfer-encoding"},
|
||||
{Name: "user-agent"},
|
||||
{Name: "vary"},
|
||||
{Name: "via"},
|
||||
{Name: "www-authenticate"},
|
||||
}
|
||||
|
||||
func newStaticTable() *headerFieldTable {
|
||||
t := &headerFieldTable{}
|
||||
t.init()
|
||||
for _, e := range staticTableEntries[:] {
|
||||
t.addEntry(e)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
var huffmanCodes = [256]uint32{
|
||||
0x1ff8,
|
||||
0x7fffd8,
|
||||
|
|
|
@ -143,7 +143,7 @@ type Server struct {
|
|||
}
|
||||
|
||||
func (s *Server) initialConnRecvWindowSize() int32 {
|
||||
if s.MaxUploadBufferPerConnection > initialWindowSize {
|
||||
if s.MaxUploadBufferPerConnection >= initialWindowSize {
|
||||
return s.MaxUploadBufferPerConnection
|
||||
}
|
||||
return 1 << 20
|
||||
|
@ -622,7 +622,9 @@ type stream struct {
|
|||
resetQueued bool // RST_STREAM queued for write; set by sc.resetStream
|
||||
gotTrailerHeader bool // HEADER frame for trailers was seen
|
||||
wroteHeaders bool // whether we wrote headers (not status 100)
|
||||
readDeadline *time.Timer // nil if unused
|
||||
writeDeadline *time.Timer // nil if unused
|
||||
closeErr error // set before cw is closed
|
||||
|
||||
trailer http.Header // accumulated trailers
|
||||
reqTrailer http.Header // handler's Request.Trailer
|
||||
|
@ -948,6 +950,8 @@ func (sc *serverConn) serve() {
|
|||
}
|
||||
case *startPushRequest:
|
||||
sc.startPush(v)
|
||||
case func(*serverConn):
|
||||
v(sc)
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected type %T", v))
|
||||
}
|
||||
|
@ -1371,6 +1375,9 @@ func (sc *serverConn) startGracefulShutdownInternal() {
|
|||
func (sc *serverConn) goAway(code ErrCode) {
|
||||
sc.serveG.check()
|
||||
if sc.inGoAway {
|
||||
if sc.goAwayCode == ErrCodeNo {
|
||||
sc.goAwayCode = code
|
||||
}
|
||||
return
|
||||
}
|
||||
sc.inGoAway = true
|
||||
|
@ -1458,6 +1465,21 @@ func (sc *serverConn) processFrame(f Frame) error {
|
|||
sc.sawFirstSettings = true
|
||||
}
|
||||
|
||||
// Discard frames for streams initiated after the identified last
|
||||
// stream sent in a GOAWAY, or all frames after sending an error.
|
||||
// We still need to return connection-level flow control for DATA frames.
|
||||
// RFC 9113 Section 6.8.
|
||||
if sc.inGoAway && (sc.goAwayCode != ErrCodeNo || f.Header().StreamID > sc.maxClientStreamID) {
|
||||
|
||||
if f, ok := f.(*DataFrame); ok {
|
||||
if sc.inflow.available() < int32(f.Length) {
|
||||
return sc.countError("data_flow", streamError(f.Header().StreamID, ErrCodeFlowControl))
|
||||
}
|
||||
sc.sendWindowUpdate(nil, int(f.Length)) // conn-level
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
switch f := f.(type) {
|
||||
case *SettingsFrame:
|
||||
return sc.processSettings(f)
|
||||
|
@ -1500,9 +1522,6 @@ func (sc *serverConn) processPing(f *PingFrame) error {
|
|||
// PROTOCOL_ERROR."
|
||||
return sc.countError("ping_on_stream", ConnectionError(ErrCodeProtocol))
|
||||
}
|
||||
if sc.inGoAway && sc.goAwayCode != ErrCodeNo {
|
||||
return nil
|
||||
}
|
||||
sc.writeFrame(FrameWriteRequest{write: writePingAck{f}})
|
||||
return nil
|
||||
}
|
||||
|
@ -1564,6 +1583,9 @@ func (sc *serverConn) closeStream(st *stream, err error) {
|
|||
panic(fmt.Sprintf("invariant; can't close stream in state %v", st.state))
|
||||
}
|
||||
st.state = stateClosed
|
||||
if st.readDeadline != nil {
|
||||
st.readDeadline.Stop()
|
||||
}
|
||||
if st.writeDeadline != nil {
|
||||
st.writeDeadline.Stop()
|
||||
}
|
||||
|
@ -1589,6 +1611,14 @@ func (sc *serverConn) closeStream(st *stream, err error) {
|
|||
|
||||
p.CloseWithError(err)
|
||||
}
|
||||
if e, ok := err.(StreamError); ok {
|
||||
if e.Cause != nil {
|
||||
err = e.Cause
|
||||
} else {
|
||||
err = errStreamClosed
|
||||
}
|
||||
}
|
||||
st.closeErr = err
|
||||
st.cw.Close() // signals Handler's CloseNotifier, unblocks writes, etc
|
||||
sc.writeSched.CloseStream(st.id)
|
||||
}
|
||||
|
@ -1685,16 +1715,6 @@ func (sc *serverConn) processSettingInitialWindowSize(val uint32) error {
|
|||
func (sc *serverConn) processData(f *DataFrame) error {
|
||||
sc.serveG.check()
|
||||
id := f.Header().StreamID
|
||||
if sc.inGoAway && (sc.goAwayCode != ErrCodeNo || id > sc.maxClientStreamID) {
|
||||
// Discard all DATA frames if the GOAWAY is due to an
|
||||
// error, or:
|
||||
//
|
||||
// Section 6.8: After sending a GOAWAY frame, the sender
|
||||
// can discard frames for streams initiated by the
|
||||
// receiver with identifiers higher than the identified
|
||||
// last stream.
|
||||
return nil
|
||||
}
|
||||
|
||||
data := f.Data()
|
||||
state, st := sc.state(id)
|
||||
|
@ -1837,19 +1857,27 @@ func (st *stream) copyTrailersToHandlerRequest() {
|
|||
}
|
||||
}
|
||||
|
||||
// onReadTimeout is run on its own goroutine (from time.AfterFunc)
|
||||
// when the stream's ReadTimeout has fired.
|
||||
func (st *stream) onReadTimeout() {
|
||||
// Wrap the ErrDeadlineExceeded to avoid callers depending on us
|
||||
// returning the bare error.
|
||||
st.body.CloseWithError(fmt.Errorf("%w", os.ErrDeadlineExceeded))
|
||||
}
|
||||
|
||||
// onWriteTimeout is run on its own goroutine (from time.AfterFunc)
|
||||
// when the stream's WriteTimeout has fired.
|
||||
func (st *stream) onWriteTimeout() {
|
||||
st.sc.writeFrameFromHandler(FrameWriteRequest{write: streamError(st.id, ErrCodeInternal)})
|
||||
st.sc.writeFrameFromHandler(FrameWriteRequest{write: StreamError{
|
||||
StreamID: st.id,
|
||||
Code: ErrCodeInternal,
|
||||
Cause: os.ErrDeadlineExceeded,
|
||||
}})
|
||||
}
|
||||
|
||||
func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error {
|
||||
sc.serveG.check()
|
||||
id := f.StreamID
|
||||
if sc.inGoAway {
|
||||
// Ignore.
|
||||
return nil
|
||||
}
|
||||
// http://tools.ietf.org/html/rfc7540#section-5.1.1
|
||||
// Streams initiated by a client MUST use odd-numbered stream
|
||||
// identifiers. [...] An endpoint that receives an unexpected
|
||||
|
@ -1952,6 +1980,9 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error {
|
|||
// (in Go 1.8), though. That's a more sane option anyway.
|
||||
if sc.hs.ReadTimeout != 0 {
|
||||
sc.conn.SetReadDeadline(time.Time{})
|
||||
if st.body != nil {
|
||||
st.readDeadline = time.AfterFunc(sc.hs.ReadTimeout, st.onReadTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
go sc.runHandler(rw, req, handler)
|
||||
|
@ -2020,9 +2051,6 @@ func (sc *serverConn) checkPriority(streamID uint32, p PriorityParam) error {
|
|||
}
|
||||
|
||||
func (sc *serverConn) processPriority(f *PriorityFrame) error {
|
||||
if sc.inGoAway {
|
||||
return nil
|
||||
}
|
||||
if err := sc.checkPriority(f.StreamID, f.PriorityParam); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -2096,12 +2124,6 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
|
|||
return nil, nil, sc.countError("bad_path_method", streamError(f.StreamID, ErrCodeProtocol))
|
||||
}
|
||||
|
||||
bodyOpen := !f.StreamEnded()
|
||||
if rp.method == "HEAD" && bodyOpen {
|
||||
// HEAD requests can't have bodies
|
||||
return nil, nil, sc.countError("head_body", streamError(f.StreamID, ErrCodeProtocol))
|
||||
}
|
||||
|
||||
rp.header = make(http.Header)
|
||||
for _, hf := range f.RegularFields() {
|
||||
rp.header.Add(sc.canonicalHeader(hf.Name), hf.Value)
|
||||
|
@ -2114,6 +2136,7 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
|
|||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
bodyOpen := !f.StreamEnded()
|
||||
if bodyOpen {
|
||||
if vv, ok := rp.header["Content-Length"]; ok {
|
||||
if cl, err := strconv.ParseUint(vv[0], 10, 63); err == nil {
|
||||
|
@ -2343,7 +2366,7 @@ func (sc *serverConn) sendWindowUpdate(st *stream, n int) {
|
|||
// a larger Read than this. Very unlikely, but we handle it here
|
||||
// rather than elsewhere for now.
|
||||
const maxUint31 = 1<<31 - 1
|
||||
for n >= maxUint31 {
|
||||
for n > maxUint31 {
|
||||
sc.sendWindowUpdate32(st, maxUint31)
|
||||
n -= maxUint31
|
||||
}
|
||||
|
@ -2463,7 +2486,15 @@ type responseWriterState struct {
|
|||
|
||||
type chunkWriter struct{ rws *responseWriterState }
|
||||
|
||||
func (cw chunkWriter) Write(p []byte) (n int, err error) { return cw.rws.writeChunk(p) }
|
||||
func (cw chunkWriter) Write(p []byte) (n int, err error) {
|
||||
n, err = cw.rws.writeChunk(p)
|
||||
if err == errStreamClosed {
|
||||
// If writing failed because the stream has been closed,
|
||||
// return the reason it was closed.
|
||||
err = cw.rws.stream.closeErr
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (rws *responseWriterState) hasTrailers() bool { return len(rws.trailers) > 0 }
|
||||
|
||||
|
@ -2502,6 +2533,10 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) {
|
|||
rws.writeHeader(200)
|
||||
}
|
||||
|
||||
if rws.handlerDone {
|
||||
rws.promoteUndeclaredTrailers()
|
||||
}
|
||||
|
||||
isHeadResp := rws.req.Method == "HEAD"
|
||||
if !rws.sentHeader {
|
||||
rws.sentHeader = true
|
||||
|
@ -2573,10 +2608,6 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) {
|
|||
return 0, nil
|
||||
}
|
||||
|
||||
if rws.handlerDone {
|
||||
rws.promoteUndeclaredTrailers()
|
||||
}
|
||||
|
||||
// only send trailers if they have actually been defined by the
|
||||
// server handler.
|
||||
hasNonemptyTrailers := rws.hasNonemptyTrailers()
|
||||
|
@ -2657,23 +2688,85 @@ func (rws *responseWriterState) promoteUndeclaredTrailers() {
|
|||
}
|
||||
}
|
||||
|
||||
func (w *responseWriter) SetReadDeadline(deadline time.Time) error {
|
||||
st := w.rws.stream
|
||||
if !deadline.IsZero() && deadline.Before(time.Now()) {
|
||||
// If we're setting a deadline in the past, reset the stream immediately
|
||||
// so writes after SetWriteDeadline returns will fail.
|
||||
st.onReadTimeout()
|
||||
return nil
|
||||
}
|
||||
w.rws.conn.sendServeMsg(func(sc *serverConn) {
|
||||
if st.readDeadline != nil {
|
||||
if !st.readDeadline.Stop() {
|
||||
// Deadline already exceeded, or stream has been closed.
|
||||
return
|
||||
}
|
||||
}
|
||||
if deadline.IsZero() {
|
||||
st.readDeadline = nil
|
||||
} else if st.readDeadline == nil {
|
||||
st.readDeadline = time.AfterFunc(deadline.Sub(time.Now()), st.onReadTimeout)
|
||||
} else {
|
||||
st.readDeadline.Reset(deadline.Sub(time.Now()))
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *responseWriter) SetWriteDeadline(deadline time.Time) error {
|
||||
st := w.rws.stream
|
||||
if !deadline.IsZero() && deadline.Before(time.Now()) {
|
||||
// If we're setting a deadline in the past, reset the stream immediately
|
||||
// so writes after SetWriteDeadline returns will fail.
|
||||
st.onWriteTimeout()
|
||||
return nil
|
||||
}
|
||||
w.rws.conn.sendServeMsg(func(sc *serverConn) {
|
||||
if st.writeDeadline != nil {
|
||||
if !st.writeDeadline.Stop() {
|
||||
// Deadline already exceeded, or stream has been closed.
|
||||
return
|
||||
}
|
||||
}
|
||||
if deadline.IsZero() {
|
||||
st.writeDeadline = nil
|
||||
} else if st.writeDeadline == nil {
|
||||
st.writeDeadline = time.AfterFunc(deadline.Sub(time.Now()), st.onWriteTimeout)
|
||||
} else {
|
||||
st.writeDeadline.Reset(deadline.Sub(time.Now()))
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *responseWriter) Flush() {
|
||||
w.FlushError()
|
||||
}
|
||||
|
||||
func (w *responseWriter) FlushError() error {
|
||||
rws := w.rws
|
||||
if rws == nil {
|
||||
panic("Header called after Handler finished")
|
||||
}
|
||||
var err error
|
||||
if rws.bw.Buffered() > 0 {
|
||||
if err := rws.bw.Flush(); err != nil {
|
||||
// Ignore the error. The frame writer already knows.
|
||||
return
|
||||
}
|
||||
err = rws.bw.Flush()
|
||||
} else {
|
||||
// The bufio.Writer won't call chunkWriter.Write
|
||||
// (writeChunk with zero bytes, so we have to do it
|
||||
// ourselves to force the HTTP response header and/or
|
||||
// final DATA frame (with END_STREAM) to be sent.
|
||||
rws.writeChunk(nil)
|
||||
_, err = chunkWriter{rws}.Write(nil)
|
||||
if err == nil {
|
||||
select {
|
||||
case <-rws.stream.cw:
|
||||
err = rws.stream.closeErr
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *responseWriter) CloseNotify() <-chan bool {
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"math"
|
||||
mathrand "math/rand"
|
||||
|
@ -258,7 +259,8 @@ func (t *Transport) initConnPool() {
|
|||
// HTTP/2 server.
|
||||
type ClientConn struct {
|
||||
t *Transport
|
||||
tconn net.Conn // usually *tls.Conn, except specialized impls
|
||||
tconn net.Conn // usually *tls.Conn, except specialized impls
|
||||
tconnClosed bool
|
||||
tlsState *tls.ConnectionState // nil only for specialized impls
|
||||
reused uint32 // whether conn is being reused; atomic
|
||||
singleUse bool // whether being used for a single http.Request
|
||||
|
@ -344,8 +346,8 @@ type clientStream struct {
|
|||
readErr error // sticky read error; owned by transportResponseBody.Read
|
||||
|
||||
reqBody io.ReadCloser
|
||||
reqBodyContentLength int64 // -1 means unknown
|
||||
reqBodyClosed bool // body has been closed; guarded by cc.mu
|
||||
reqBodyContentLength int64 // -1 means unknown
|
||||
reqBodyClosed chan struct{} // guarded by cc.mu; non-nil on Close, closed when done
|
||||
|
||||
// owned by writeRequest:
|
||||
sentEndStream bool // sent an END_STREAM flag to the peer
|
||||
|
@ -385,9 +387,8 @@ func (cs *clientStream) abortStreamLocked(err error) {
|
|||
cs.abortErr = err
|
||||
close(cs.abort)
|
||||
})
|
||||
if cs.reqBody != nil && !cs.reqBodyClosed {
|
||||
cs.reqBody.Close()
|
||||
cs.reqBodyClosed = true
|
||||
if cs.reqBody != nil {
|
||||
cs.closeReqBodyLocked()
|
||||
}
|
||||
// TODO(dneil): Clean up tests where cs.cc.cond is nil.
|
||||
if cs.cc.cond != nil {
|
||||
|
@ -400,13 +401,24 @@ func (cs *clientStream) abortRequestBodyWrite() {
|
|||
cc := cs.cc
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
if cs.reqBody != nil && !cs.reqBodyClosed {
|
||||
cs.reqBody.Close()
|
||||
cs.reqBodyClosed = true
|
||||
if cs.reqBody != nil && cs.reqBodyClosed == nil {
|
||||
cs.closeReqBodyLocked()
|
||||
cc.cond.Broadcast()
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *clientStream) closeReqBodyLocked() {
|
||||
if cs.reqBodyClosed != nil {
|
||||
return
|
||||
}
|
||||
cs.reqBodyClosed = make(chan struct{})
|
||||
reqBodyClosed := cs.reqBodyClosed
|
||||
go func() {
|
||||
cs.reqBody.Close()
|
||||
close(reqBodyClosed)
|
||||
}()
|
||||
}
|
||||
|
||||
type stickyErrWriter struct {
|
||||
conn net.Conn
|
||||
timeout time.Duration
|
||||
|
@ -490,6 +502,15 @@ func authorityAddr(scheme string, authority string) (addr string) {
|
|||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
var retryBackoffHook func(time.Duration) *time.Timer
|
||||
|
||||
func backoffNewTimer(d time.Duration) *time.Timer {
|
||||
if retryBackoffHook != nil {
|
||||
return retryBackoffHook(d)
|
||||
}
|
||||
return time.NewTimer(d)
|
||||
}
|
||||
|
||||
// RoundTripOpt is like RoundTrip, but takes options.
|
||||
func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
|
||||
if !(req.URL.Scheme == "https" || (req.URL.Scheme == "http" && t.AllowHTTP)) {
|
||||
|
@ -515,11 +536,14 @@ func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res
|
|||
}
|
||||
backoff := float64(uint(1) << (uint(retry) - 1))
|
||||
backoff += backoff * (0.1 * mathrand.Float64())
|
||||
d := time.Second * time.Duration(backoff)
|
||||
timer := backoffNewTimer(d)
|
||||
select {
|
||||
case <-time.After(time.Second * time.Duration(backoff)):
|
||||
case <-timer.C:
|
||||
t.vlogf("RoundTrip retrying after failure: %v", err)
|
||||
continue
|
||||
case <-req.Context().Done():
|
||||
timer.Stop()
|
||||
err = req.Context().Err()
|
||||
}
|
||||
}
|
||||
|
@ -921,10 +945,10 @@ func (cc *ClientConn) onIdleTimeout() {
|
|||
cc.closeIfIdle()
|
||||
}
|
||||
|
||||
func (cc *ClientConn) closeConn() error {
|
||||
func (cc *ClientConn) closeConn() {
|
||||
t := time.AfterFunc(250*time.Millisecond, cc.forceCloseConn)
|
||||
defer t.Stop()
|
||||
return cc.tconn.Close()
|
||||
cc.tconn.Close()
|
||||
}
|
||||
|
||||
// A tls.Conn.Close can hang for a long time if the peer is unresponsive.
|
||||
|
@ -990,7 +1014,8 @@ func (cc *ClientConn) Shutdown(ctx context.Context) error {
|
|||
shutdownEnterWaitStateHook()
|
||||
select {
|
||||
case <-done:
|
||||
return cc.closeConn()
|
||||
cc.closeConn()
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
cc.mu.Lock()
|
||||
// Free the goroutine above
|
||||
|
@ -1027,7 +1052,7 @@ func (cc *ClientConn) sendGoAway() error {
|
|||
|
||||
// closes the client connection immediately. In-flight requests are interrupted.
|
||||
// err is sent to streams.
|
||||
func (cc *ClientConn) closeForError(err error) error {
|
||||
func (cc *ClientConn) closeForError(err error) {
|
||||
cc.mu.Lock()
|
||||
cc.closed = true
|
||||
for _, cs := range cc.streams {
|
||||
|
@ -1035,7 +1060,7 @@ func (cc *ClientConn) closeForError(err error) error {
|
|||
}
|
||||
cc.cond.Broadcast()
|
||||
cc.mu.Unlock()
|
||||
return cc.closeConn()
|
||||
cc.closeConn()
|
||||
}
|
||||
|
||||
// Close closes the client connection immediately.
|
||||
|
@ -1043,16 +1068,17 @@ func (cc *ClientConn) closeForError(err error) error {
|
|||
// In-flight requests are interrupted. For a graceful shutdown, use Shutdown instead.
|
||||
func (cc *ClientConn) Close() error {
|
||||
err := errors.New("http2: client connection force closed via ClientConn.Close")
|
||||
return cc.closeForError(err)
|
||||
cc.closeForError(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// closes the client connection immediately. In-flight requests are interrupted.
|
||||
func (cc *ClientConn) closeForLostPing() error {
|
||||
func (cc *ClientConn) closeForLostPing() {
|
||||
err := errors.New("http2: client connection lost")
|
||||
if f := cc.t.CountError; f != nil {
|
||||
f("conn_close_lost_ping")
|
||||
}
|
||||
return cc.closeForError(err)
|
||||
cc.closeForError(err)
|
||||
}
|
||||
|
||||
// errRequestCanceled is a copy of net/http's errRequestCanceled because it's not
|
||||
|
@ -1062,7 +1088,7 @@ var errRequestCanceled = errors.New("net/http: request canceled")
|
|||
func commaSeparatedTrailers(req *http.Request) (string, error) {
|
||||
keys := make([]string, 0, len(req.Trailer))
|
||||
for k := range req.Trailer {
|
||||
k = http.CanonicalHeaderKey(k)
|
||||
k = canonicalHeader(k)
|
||||
switch k {
|
||||
case "Transfer-Encoding", "Trailer", "Content-Length":
|
||||
return "", fmt.Errorf("invalid Trailer key %q", k)
|
||||
|
@ -1430,11 +1456,19 @@ func (cs *clientStream) cleanupWriteRequest(err error) {
|
|||
// and in multiple cases: server replies <=299 and >299
|
||||
// while still writing request body
|
||||
cc.mu.Lock()
|
||||
mustCloseBody := false
|
||||
if cs.reqBody != nil && cs.reqBodyClosed == nil {
|
||||
mustCloseBody = true
|
||||
cs.reqBodyClosed = make(chan struct{})
|
||||
}
|
||||
bodyClosed := cs.reqBodyClosed
|
||||
cs.reqBodyClosed = true
|
||||
cc.mu.Unlock()
|
||||
if !bodyClosed && cs.reqBody != nil {
|
||||
if mustCloseBody {
|
||||
cs.reqBody.Close()
|
||||
close(bodyClosed)
|
||||
}
|
||||
if bodyClosed != nil {
|
||||
<-bodyClosed
|
||||
}
|
||||
|
||||
if err != nil && cs.sentEndStream {
|
||||
|
@ -1591,7 +1625,7 @@ func (cs *clientStream) writeRequestBody(req *http.Request) (err error) {
|
|||
|
||||
var sawEOF bool
|
||||
for !sawEOF {
|
||||
n, err := body.Read(buf[:len(buf)])
|
||||
n, err := body.Read(buf)
|
||||
if hasContentLen {
|
||||
remainLen -= int64(n)
|
||||
if remainLen == 0 && err == nil {
|
||||
|
@ -1614,7 +1648,7 @@ func (cs *clientStream) writeRequestBody(req *http.Request) (err error) {
|
|||
}
|
||||
if err != nil {
|
||||
cc.mu.Lock()
|
||||
bodyClosed := cs.reqBodyClosed
|
||||
bodyClosed := cs.reqBodyClosed != nil
|
||||
cc.mu.Unlock()
|
||||
switch {
|
||||
case bodyClosed:
|
||||
|
@ -1709,7 +1743,7 @@ func (cs *clientStream) awaitFlowControl(maxBytes int) (taken int32, err error)
|
|||
if cc.closed {
|
||||
return 0, errClientConnClosed
|
||||
}
|
||||
if cs.reqBodyClosed {
|
||||
if cs.reqBodyClosed != nil {
|
||||
return 0, errStopReqBodyWrite
|
||||
}
|
||||
select {
|
||||
|
@ -1894,7 +1928,7 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
|
|||
|
||||
// Header list size is ok. Write the headers.
|
||||
enumerateHeaders(func(name, value string) {
|
||||
name, ascii := asciiToLower(name)
|
||||
name, ascii := lowerHeader(name)
|
||||
if !ascii {
|
||||
// Skip writing invalid headers. Per RFC 7540, Section 8.1.2, header
|
||||
// field names have to be ASCII characters (just as in HTTP/1.x).
|
||||
|
@ -1947,7 +1981,7 @@ func (cc *ClientConn) encodeTrailers(trailer http.Header) ([]byte, error) {
|
|||
}
|
||||
|
||||
for k, vv := range trailer {
|
||||
lowKey, ascii := asciiToLower(k)
|
||||
lowKey, ascii := lowerHeader(k)
|
||||
if !ascii {
|
||||
// Skip writing invalid headers. Per RFC 7540, Section 8.1.2, header
|
||||
// field names have to be ASCII characters (just as in HTTP/1.x).
|
||||
|
@ -2005,7 +2039,7 @@ func (cc *ClientConn) forgetStreamID(id uint32) {
|
|||
// wake up RoundTrip if there is a pending request.
|
||||
cc.cond.Broadcast()
|
||||
|
||||
closeOnIdle := cc.singleUse || cc.doNotReuse || cc.t.disableKeepAlives()
|
||||
closeOnIdle := cc.singleUse || cc.doNotReuse || cc.t.disableKeepAlives() || cc.goAway != nil
|
||||
if closeOnIdle && cc.streamsReserved == 0 && len(cc.streams) == 0 {
|
||||
if VerboseLogs {
|
||||
cc.vlogf("http2: Transport closing idle conn %p (forSingleUse=%v, maxStream=%v)", cc, cc.singleUse, cc.nextStreamID-2)
|
||||
|
@ -2081,6 +2115,7 @@ func (rl *clientConnReadLoop) cleanup() {
|
|||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
cc.closed = true
|
||||
|
||||
for _, cs := range cc.streams {
|
||||
select {
|
||||
case <-cs.peerClosed:
|
||||
|
@ -2279,7 +2314,7 @@ func (rl *clientConnReadLoop) handleResponse(cs *clientStream, f *MetaHeadersFra
|
|||
Status: status + " " + http.StatusText(statusCode),
|
||||
}
|
||||
for _, hf := range regularFields {
|
||||
key := http.CanonicalHeaderKey(hf.Name)
|
||||
key := canonicalHeader(hf.Name)
|
||||
if key == "Trailer" {
|
||||
t := res.Trailer
|
||||
if t == nil {
|
||||
|
@ -2287,7 +2322,7 @@ func (rl *clientConnReadLoop) handleResponse(cs *clientStream, f *MetaHeadersFra
|
|||
res.Trailer = t
|
||||
}
|
||||
foreachHeaderElement(hf.Value, func(v string) {
|
||||
t[http.CanonicalHeaderKey(v)] = nil
|
||||
t[canonicalHeader(v)] = nil
|
||||
})
|
||||
} else {
|
||||
vv := header[key]
|
||||
|
@ -2392,7 +2427,7 @@ func (rl *clientConnReadLoop) processTrailers(cs *clientStream, f *MetaHeadersFr
|
|||
|
||||
trailer := make(http.Header)
|
||||
for _, hf := range f.RegularFields() {
|
||||
key := http.CanonicalHeaderKey(hf.Name)
|
||||
key := canonicalHeader(hf.Name)
|
||||
trailer[key] = append(trailer[key], hf.Value)
|
||||
}
|
||||
cs.trailer = trailer
|
||||
|
@ -2674,7 +2709,6 @@ func (rl *clientConnReadLoop) processGoAway(f *GoAwayFrame) error {
|
|||
if fn := cc.t.CountError; fn != nil {
|
||||
fn("recv_goaway_" + f.ErrCode.stringToken())
|
||||
}
|
||||
|
||||
}
|
||||
cc.setGoAway(f)
|
||||
return nil
|
||||
|
@ -2964,7 +2998,11 @@ func (gz *gzipReader) Read(p []byte) (n int, err error) {
|
|||
}
|
||||
|
||||
func (gz *gzipReader) Close() error {
|
||||
return gz.body.Close()
|
||||
if err := gz.body.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
gz.zerr = fs.ErrClosed
|
||||
return nil
|
||||
}
|
||||
|
||||
type errorReader struct{ err error }
|
||||
|
@ -3028,7 +3066,7 @@ func traceGotConn(req *http.Request, cc *ClientConn, reused bool) {
|
|||
cc.mu.Lock()
|
||||
ci.WasIdle = len(cc.streams) == 0 && reused
|
||||
if ci.WasIdle && !cc.lastActive.IsZero() {
|
||||
ci.IdleTime = time.Now().Sub(cc.lastActive)
|
||||
ci.IdleTime = time.Since(cc.lastActive)
|
||||
}
|
||||
cc.mu.Unlock()
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !aix && !linux && (ppc64 || ppc64le)
|
||||
// +build !aix
|
||||
// +build !linux
|
||||
// +build ppc64 ppc64le
|
||||
|
||||
package cpu
|
||||
|
||||
func archInit() {
|
||||
PPC64.IsPOWER8 = true
|
||||
Initialized = true
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build (darwin || freebsd || netbsd || openbsd) && gc
|
||||
// +build darwin freebsd netbsd openbsd
|
||||
// +build gc
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
//
|
||||
// System call support for ppc64, BSD
|
||||
//
|
||||
|
||||
// Just jump to package syscall's implementation for all these functions.
|
||||
// The runtime may know about them.
|
||||
|
||||
TEXT ·Syscall(SB),NOSPLIT,$0-56
|
||||
JMP syscall·Syscall(SB)
|
||||
|
||||
TEXT ·Syscall6(SB),NOSPLIT,$0-80
|
||||
JMP syscall·Syscall6(SB)
|
||||
|
||||
TEXT ·Syscall9(SB),NOSPLIT,$0-104
|
||||
JMP syscall·Syscall9(SB)
|
||||
|
||||
TEXT ·RawSyscall(SB),NOSPLIT,$0-56
|
||||
JMP syscall·RawSyscall(SB)
|
||||
|
||||
TEXT ·RawSyscall6(SB),NOSPLIT,$0-80
|
||||
JMP syscall·RawSyscall6(SB)
|
|
@ -2,8 +2,8 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos
|
||||
|
||||
package unix
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue