From a13f8a039a9cb6d4bd24dee4b8ccb38ddf8e12b7 Mon Sep 17 00:00:00 2001 From: Gregor Noczinski Date: Mon, 26 Aug 2024 18:27:55 +0200 Subject: [PATCH] Added dummy repository because we'll remove the local environment afterwards again for windows - it does not make sense because Impersonation isn't supported at Windows by design. --- go.mod | 8 + go.sum | 26 ++ pkg/configuration/environment-dummy.go | 87 +++++++ pkg/dummy/dummy.go | 319 +++++++++++++++++++++++++ pkg/environment/dummy-repository.go | 93 +++++++ pkg/environment/dummy-tty.go | 89 +++++++ pkg/environment/dummy.go | 121 ++++++++++ pkg/environment/facade-repository.go | 11 + pkg/environment/local-repository.go | 5 + pkg/environment/repository.go | 5 + pkg/service/service-authorization.go | 26 ++ pkg/service/service_linux.go | 6 - pkg/service/service_windows.go | 6 - 13 files changed, 790 insertions(+), 12 deletions(-) create mode 100644 pkg/configuration/environment-dummy.go create mode 100644 pkg/dummy/dummy.go create mode 100644 pkg/environment/dummy-repository.go create mode 100644 pkg/environment/dummy-tty.go create mode 100644 pkg/environment/dummy.go diff --git a/go.mod b/go.mod index 91727dc..e4623b1 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,8 @@ require ( github.com/echocat/slf4g v1.6.1 github.com/echocat/slf4g/native v1.6.1 github.com/fsnotify/fsnotify v1.7.0 + github.com/gdamore/encoding v1.0.0 + github.com/gdamore/tcell/v2 v2.7.4 github.com/gliderlabs/ssh v0.3.7 github.com/google/uuid v1.6.0 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 @@ -19,6 +21,7 @@ require ( github.com/msteinert/pam/v2 v2.0.0 github.com/otiai10/copy v1.14.0 github.com/pkg/sftp v1.13.6 + github.com/rivo/tview v0.0.0-20240818110301-fd649dbf1223 github.com/shirou/gopsutil v3.21.11+incompatible github.com/stretchr/testify v1.9.0 github.com/tg123/go-htpasswd v1.2.2 @@ -40,9 +43,12 @@ require ( github.com/huandu/xstrings v1.5.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/kr/fs v0.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.7.0 // indirect @@ -50,5 +56,7 @@ require ( github.com/tklauser/numcpus v0.8.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/sync v0.8.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 6321c34..e1d014e 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,10 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= +github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= @@ -58,6 +62,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE= github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= @@ -78,6 +86,12 @@ github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/tview v0.0.0-20240818110301-fd649dbf1223 h1:N+DggyldbUDqFlk0b8JeRjB9zGpmQ8wiKpq+VBbzRso= +github.com/rivo/tview v0.0.0-20240818110301-fd649dbf1223/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= @@ -115,15 +129,18 @@ golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4 golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -134,21 +151,30 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/pkg/configuration/environment-dummy.go b/pkg/configuration/environment-dummy.go new file mode 100644 index 0000000..ad97a7d --- /dev/null +++ b/pkg/configuration/environment-dummy.go @@ -0,0 +1,87 @@ +package configuration + +import ( + "gopkg.in/yaml.v3" + + "github.com/engity-com/bifroest/pkg/template" +) + +var ( + DefaultEnvironmentDummyLoginAllowed = template.BoolOf(true) + DefaultEnvironmentDummyIntroduction = template.MustNewString(`[::bu]Welcome to [:::https://github.com/engity-com/bifroest]Engity's Bifröst![:::-][::-] + +You've entered Bifröst's dummy environment. It is meant to demonstrate basic interactions with [:::https://en.wikipedia.org/wiki/Pseudoterminal]PTYs[:::-]. + +You can either hit different keys and see what you've hit within the [::i]Properties[::-] section (below) or draw on the white panel (on the right) with your mouse. + +Supported keys: +* [Ctrl[]+[C[] or [Q[] to exit +* [C[] to clean the white draw panel (right) +`) + DefaultEnvironmentDummyIntroductionStyled = template.BoolOf(true) + + _ = RegisterEnvironmentV(func() EnvironmentV { + return &EnvironmentDummy{} + }) +) + +type EnvironmentDummy struct { + LoginAllowed template.Bool `yaml:"loginAllowed,omitempty"` + Introduction template.String `yaml:"introduction,omitempty"` + IntroductionStyled template.Bool `yaml:"introductionStyled,omitempty"` +} + +func (this *EnvironmentDummy) SetDefaults() error { + return setDefaults(this, + fixedDefault("loginAllowed", func(v *EnvironmentDummy) *template.Bool { return &v.LoginAllowed }, DefaultEnvironmentDummyLoginAllowed), + fixedDefault("introduction", func(v *EnvironmentDummy) *template.String { return &v.Introduction }, DefaultEnvironmentDummyIntroduction), + fixedDefault("introductionStyled", func(v *EnvironmentDummy) *template.Bool { return &v.IntroductionStyled }, DefaultEnvironmentDummyIntroductionStyled), + ) +} + +func (this *EnvironmentDummy) Trim() error { + return trim(this, + noopTrim[EnvironmentDummy]("loginAllowed"), + noopTrim[EnvironmentDummy]("introduction"), + noopTrim[EnvironmentDummy]("introductionStyled"), + ) +} + +func (this *EnvironmentDummy) Validate() error { + return validate(this, + noopValidate[EnvironmentDummy]("loginAllowed"), + noopValidate[EnvironmentDummy]("introduction"), + noopValidate[EnvironmentDummy]("introductionStyled"), + ) +} + +func (this *EnvironmentDummy) UnmarshalYAML(node *yaml.Node) error { + return unmarshalYAML(this, node, func(target *EnvironmentDummy, node *yaml.Node) error { + type raw EnvironmentDummy + return node.Decode((*raw)(target)) + }) +} + +func (this EnvironmentDummy) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case EnvironmentDummy: + return this.isEqualTo(&v) + case *EnvironmentDummy: + return this.isEqualTo(v) + default: + return false + } +} + +func (this EnvironmentDummy) isEqualTo(other *EnvironmentDummy) bool { + return isEqual(&this.LoginAllowed, &other.LoginAllowed) && + isEqual(&this.Introduction, &other.Introduction) && + isEqual(&this.IntroductionStyled, &other.IntroductionStyled) +} + +func (this EnvironmentDummy) Types() []string { + return []string{"dummy"} +} diff --git a/pkg/dummy/dummy.go b/pkg/dummy/dummy.go new file mode 100644 index 0000000..9767d8a --- /dev/null +++ b/pkg/dummy/dummy.go @@ -0,0 +1,319 @@ +package dummy + +import ( + "fmt" + "io" + "strings" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" + + "github.com/engity-com/bifroest/pkg/errors" +) + +type Dummy struct { + Screen tcell.Screen + + Introduction string + ShowEvents bool +} + +func (this *Dummy) Execute() error { + s := this.Screen + if s == nil { + panic("Screen nil") + } + + pg, reset := this.createPlayground() + + var app *tview.Application + onKeyEvent := func(e *tcell.EventKey) *tcell.EventKey { + if e.Key() == tcell.KeyEscape || e.Rune() == 'q' || e.Rune() == 'Q' || e.Key() == tcell.KeyCtrlC || e.Key() == tcell.KeyCtrlD { + app.Stop() + } + if e.Rune() == 'c' || e.Rune() == 'C' { + reset() + } + + return e + } + onMouseEvent := func(e *tcell.EventMouse, action tview.MouseAction) (*tcell.EventMouse, tview.MouseAction) { + return e, action + } + + container := tview.NewGrid(). + SetRows(0). + SetColumns(45, 0). + AddItem(this.createSide(&app, &onKeyEvent, &onMouseEvent), 0, 0, 1, 1, 0, 0, false). + AddItem(pg, 0, 1, 1, 1, 0, 0, true) + + app = tview.NewApplication(). + SetScreen(s). + SetInputCapture(onKeyEvent). + SetMouseCapture(onMouseEvent). + EnableMouse(true). + EnablePaste(true). + SetRoot(container, true). + SetFocus(container) + + if err := app.Run(); err != nil { + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { + return nil + } + return fmt.Errorf("cannot run application: %w", err) + } + + s.Clear() + s.ShowCursor(0, 0) + s.Fini() + + return nil +} + +func (this *Dummy) createSide( + app **tview.Application, + onKeyEvent *func(e *tcell.EventKey) *tcell.EventKey, + onMouseEvent *func(e *tcell.EventMouse, action tview.MouseAction) (*tcell.EventMouse, tview.MouseAction), +) tview.Primitive { + result := tview.NewFlex(). + SetDirection(tview.FlexRow) + result. + SetBackgroundColor(tcell.ColorNavy). + SetBorderPadding(0, 0, 0, 1) + + this.addIntroduction(result) + this.addProperties(result, app, onKeyEvent, onMouseEvent) + + return result +} + +func (this *Dummy) addIntroduction(to *tview.Flex) { + if v := this.Introduction; v != "" { + this.addSectionHeader(to, "Introduction") + view := tview.NewTextView(). + SetDynamicColors(true). + SetText(strings.TrimSpace(v)) + to.AddItem(view, 0, 1, false) + } +} + +func (this *Dummy) addSectionHeader(to *tview.Flex, title string) { + header := tview.NewTextView(). + SetText(title) + header.SetBackgroundColor(tcell.ColorNavy) + to.AddItem(header, 1, 0, false) +} + +func (this *Dummy) addProperties(to *tview.Flex, app **tview.Application, onKeyEvent *func(e *tcell.EventKey) *tcell.EventKey, onMouseEvent *func(e *tcell.EventMouse, action tview.MouseAction) (*tcell.EventMouse, tview.MouseAction)) { + newLabelCell := func(label string) *tview.TableCell { + return tview.NewTableCell(label). + SetAlign(tview.AlignLeft | tview.AlignTop) + } + newValueCell := func(value string) *tview.TableCell { + return tview.NewTableCell(value). + SetAlign(tview.AlignLeft | tview.AlignTop) + } + + this.addSectionHeader(to, "Properties") + + properties := tview.NewTable() + row := 0 + to.AddItem(properties, 0, 1, false) + + if this.ShowEvents { + { + value := newValueCell("") + properties.SetCell(row, 0, newLabelCell("Last key:")). + SetCell(row, 1, value) + row++ + + old := *onKeyEvent + *onKeyEvent = func(e *tcell.EventKey) *tcell.EventKey { + if e.Key() == tcell.KeyEscape || e.Rune() == 'q' || e.Rune() == 'Q' || e.Key() == tcell.KeyCtrlC || e.Key() == tcell.KeyCtrlD { + if app != nil { + (*app).Stop() + } + } + + value.Text = tview.Escape(e.Name()) + if value.Text == "" { + value.Text = "None" + } + + return old(e) + } + (*onKeyEvent)(&tcell.EventKey{}) + } + + { + value := newValueCell("") + properties.SetCell(row, 0, newLabelCell("Last click at: ")). + SetCell(row, 1, value) + row++ + old := *onMouseEvent + *onMouseEvent = func(e *tcell.EventMouse, action tview.MouseAction) (*tcell.EventMouse, tview.MouseAction) { + x, v := e.Position() + value.Text = fmt.Sprintf("%dx%d", x, v) + return old(e, action) + } + (*onMouseEvent)(&tcell.EventMouse{}, 0) + + } + } +} + +type playgroundPixel struct { + left bool + middle bool + right bool +} + +type playground struct { + *tview.Box + matrix [][]playgroundPixel + leftDown bool + middleDown bool + rightDown bool +} + +func (this *playground) reset() { + for y, row := range this.matrix { + for x := range row { + this.matrix[y][x] = playgroundPixel{} + } + } +} + +func (this *playground) SetRect(x, y, width, height int) { + this.Box.SetRect(x, y, width, height) + if len(this.matrix) > height { + this.matrix = this.matrix[:height] + } + if len(this.matrix) < height { + oldMatrix := this.matrix + this.matrix = make([][]playgroundPixel, height) + copy(this.matrix, oldMatrix) + } + + for rowI := 0; rowI < height; rowI++ { + row := this.matrix[rowI] + + if len(row) > width { + row = row[:width] + this.matrix[rowI] = row + } else if len(row) < width { + oldRow := row + row = make([]playgroundPixel, width) + copy(row, oldRow) + this.matrix[rowI] = row + } + } +} + +func (this *playground) Draw(s tcell.Screen) { + style := tcell.StyleDefault. + Background(tcell.ColorWhite). + Foreground(tcell.ColorBlack) + + this.DrawForSubclass(s, this) + viewX, viewY, _, _ := this.GetInnerRect() + for y, row := range this.matrix { + for x, col := range row { + ts := style + c := ' ' + if col.left { + ts = ts.Background(tcell.ColorBlue).Foreground(tcell.ColorWhite) + c = 'L' + } else if col.right { + ts = ts.Background(tcell.ColorYellow).Foreground(tcell.ColorWhite) + c = 'R' + } else if col.middle { + ts = ts.Background(tcell.ColorPurple).Foreground(tcell.ColorWhite) + c = 'M' + } + s.SetContent(x+viewX, y+viewY, c, nil, ts) + } + } +} + +func (this *playground) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { + return this.WrapMouseHandler(func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { + x, y := event.Position() + rectX, rectY, _, _ := this.GetInnerRect() + if !this.InRect(x, y) { + return false, nil + } + x -= rectX + y -= rectY + + switch action { + case tview.MouseLeftUp: + this.leftDown = false + return false, nil + case tview.MouseLeftDown: + this.leftDown = true + + case tview.MouseMiddleUp: + this.middleDown = false + return false, nil + case tview.MouseMiddleDown: + this.middleDown = true + + case tview.MouseRightUp: + this.rightDown = false + return false, nil + case tview.MouseRightDown: + this.rightDown = true + + case tview.MouseMove: + if !this.leftDown && !this.middleDown && !this.rightDown { + return false, nil + } + + default: + return false, nil + } + + this.matrix[y][x] = playgroundPixel{ + left: this.leftDown, + middle: this.middleDown, + right: this.rightDown, + } + + return true, this + }) +} + +func (this *Dummy) createPlayground() (tview.Primitive, func()) { + instance := &playground{ + Box: tview.NewBox(), + } + + instance. + SetBackgroundColor(tcell.ColorWhite) + + return instance, instance.reset +} + +func (this *Dummy) evaluateButtons(e *tcell.EventMouse) []string { + var mbs []string + m := func(match tcell.ButtonMask, name string) { + if e.Buttons()&match != 0 { + mbs = append(mbs, name) + } + } + m(tcell.Button1, "Button1") + m(tcell.Button2, "Button2") + m(tcell.Button3, "Button3") + m(tcell.Button4, "Button4") + m(tcell.Button5, "Button5") + m(tcell.Button6, "Button6") + m(tcell.Button7, "Button7") + m(tcell.Button8, "Button8") + m(tcell.WheelUp, "WheelUp") + m(tcell.WheelDown, "WheelDown") + m(tcell.WheelLeft, "WheelLeft") + m(tcell.WheelRight, "WheelRight") + + return mbs +} diff --git a/pkg/environment/dummy-repository.go b/pkg/environment/dummy-repository.go new file mode 100644 index 0000000..5fe2670 --- /dev/null +++ b/pkg/environment/dummy-repository.go @@ -0,0 +1,93 @@ +package environment + +import ( + "context" + "fmt" + + log "github.com/echocat/slf4g" + "github.com/gliderlabs/ssh" + + "github.com/engity-com/bifroest/pkg/configuration" + "github.com/engity-com/bifroest/pkg/errors" + "github.com/engity-com/bifroest/pkg/session" +) + +var ( + _ = RegisterRepository(NewDummyRepository) +) + +type DummyRepository struct { + flow configuration.FlowName + conf *configuration.EnvironmentDummy + + Logger log.Logger +} + +func NewDummyRepository(_ context.Context, flow configuration.FlowName, conf *configuration.EnvironmentDummy) (*DummyRepository, error) { + fail := func(err error) (*DummyRepository, error) { + return nil, err + } + failf := func(msg string, args ...any) (*DummyRepository, error) { + return fail(fmt.Errorf(msg, args...)) + } + + if conf == nil { + return failf("nil configuration") + } + + result := DummyRepository{ + flow: flow, + conf: conf, + } + + return &result, nil +} + +func (this *DummyRepository) WillBeAccepted(req Request) (ok bool, err error) { + fail := func(err error) (bool, error) { + return false, err + } + + if ok, err = this.conf.LoginAllowed.Render(req); err != nil { + return fail(fmt.Errorf("cannot evaluate if user is allowed to login or not: %w", err)) + } + + return ok, nil +} + +func (this *DummyRepository) DoesSupportPty(Request, ssh.Pty) (bool, error) { + return true, nil +} + +func (this *DummyRepository) Ensure(req Request) (Environment, error) { + fail := func(err error) (Environment, error) { + return nil, err + } + failf := func(t errors.Type, msg string, args ...any) (Environment, error) { + return fail(errors.Newf(t, msg, args...)) + } + + if ok, err := this.WillBeAccepted(req); err != nil { + return fail(err) + } else if !ok { + return fail(ErrNotAcceptable) + } + + sess := req.Authorization().FindSession() + if sess == nil { + return failf(errors.System, "authorization without session") + } + + return this.FindBySession(req.Context(), sess, nil) +} + +func (this *DummyRepository) FindBySession(_ context.Context, sess session.Session, _ *FindOpts) (Environment, error) { + return &dummy{ + this, + sess, + }, nil +} + +func (this *DummyRepository) Close() error { + return nil +} diff --git a/pkg/environment/dummy-tty.go b/pkg/environment/dummy-tty.go new file mode 100644 index 0000000..ebdde05 --- /dev/null +++ b/pkg/environment/dummy-tty.go @@ -0,0 +1,89 @@ +package environment + +import ( + "io" + "sync" + + "github.com/gdamore/tcell/v2" + "github.com/gliderlabs/ssh" +) + +type dummyTtySession interface { + io.ReadWriter + Context() ssh.Context +} + +type dummyTty struct { + session dummyTtySession + + environment *dummy + windowChannel <-chan ssh.Window + + onNotifyResize func() + window ssh.Window + + mutex sync.RWMutex +} + +func (this *dummyTty) Start() error { + go func() { + for { + select { + case <-this.session.Context().Done(): + return + case v, ok := <-this.windowChannel: + if !ok { + return + } + this.windowChanged(v) + } + } + }() + + return nil +} + +func (this *dummyTty) Stop() error { + return nil +} + +func (this *dummyTty) Drain() error { + return nil +} + +func (this *dummyTty) NotifyResize(cb func()) { + this.mutex.Lock() + defer this.mutex.Unlock() + this.onNotifyResize = cb +} + +func (this *dummyTty) windowChanged(v ssh.Window) { + this.mutex.Lock() + defer this.mutex.Unlock() + this.window = v + if cb := this.onNotifyResize; cb != nil { + cb() + } +} + +func (this *dummyTty) WindowSize() (tcell.WindowSize, error) { + this.mutex.RLock() + defer this.mutex.RUnlock() + + return tcell.WindowSize{ + Width: this.window.Width, + Height: this.window.Height, + }, nil +} + +func (this *dummyTty) Read(p []byte) (n int, err error) { + return this.session.Read(p) +} + +func (this *dummyTty) Write(p []byte) (n int, err error) { + return this.session.Write(p) +} + +func (this *dummyTty) Close() error { + return nil +} diff --git a/pkg/environment/dummy.go b/pkg/environment/dummy.go new file mode 100644 index 0000000..ff12831 --- /dev/null +++ b/pkg/environment/dummy.go @@ -0,0 +1,121 @@ +package environment + +import ( + "bytes" + "context" + "fmt" + "io" + + gencoding "github.com/gdamore/encoding" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" + + dpkg "github.com/engity-com/bifroest/pkg/dummy" + "github.com/engity-com/bifroest/pkg/errors" + "github.com/engity-com/bifroest/pkg/session" +) + +func init() { + tcell.RegisterEncoding("UTF-16", gencoding.UTF8) +} + +type dummy struct { + repository *DummyRepository + session session.Session +} + +func (this *dummy) Session() session.Session { + return this.session +} + +func (this *dummy) Banner(Request) (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(nil)), nil +} + +func (this *dummy) introduction(req Request) (string, error) { + b, err := this.repository.conf.Introduction.Render(req) + if err != nil { + return "", err + } + + return b, nil +} + +func (this *dummy) Run(t Task) (exitCode int, rErr error) { + switch t.TaskType() { + case TaskTypeShell: + // Ok. + case TaskTypeSftp: + return -1, fmt.Errorf("sftp not supported by dummy environment") + default: + return -1, fmt.Errorf("illegal task type: %v", t.TaskType()) + } + + ptyReq, winCh, isPty := t.SshSession().Pty() + if !isPty { + return -1, fmt.Errorf("dummy environment requires a PTY, but current SSH sessions does not support it") + } + + introduction, err := this.repository.conf.Introduction.Render(t) + if err != nil { + return -1, fmt.Errorf("cannot render introduction: %w", err) + } + if introductionStyled, err := this.repository.conf.IntroductionStyled.Render(t); err != nil { + return -1, fmt.Errorf("cannot render introduction: %w", err) + } else if !introductionStyled { + introduction = tview.Escape(introduction) + } + + tty := &dummyTty{ + session: t.SshSession(), + environment: this, + windowChannel: winCh, + window: ptyReq.Window, + } + + ti, err := tcell.LookupTerminfo(ptyReq.Term) + if err != nil { + return -1, fmt.Errorf("cannot resolve terminfo: %w", err) + } + + s, err := tcell.NewTerminfoScreenFromTtyTerminfo(tty, ti) + if err != nil { + return -1, fmt.Errorf("cannot create screen: %w", err) + } + + d := &dpkg.Dummy{ + Screen: s, + Introduction: introduction, + ShowEvents: true, + } + + if err := d.Execute(); err != nil { + return -1, err + } + + s.Fini() + _, _ = fmt.Fprintf(tty, "Bye!\n\n") + return 0, nil +} + +func (this *dummy) Dispose(ctx context.Context) (bool, error) { + fail := func(err error) (bool, error) { + return false, errors.Newf(errors.System, "cannot dispose environment: %w", err) + } + + if sess := this.session; sess != nil { + if err := sess.SetEnvironmentToken(ctx, nil); err != nil { + return fail(err) + } + } + + return true, nil +} + +func (this *dummy) IsPortForwardingAllowed(_ string, _ uint32) (bool, error) { + return false, nil +} + +func (this *dummy) NewDestinationConnection(ctx context.Context, host string, port uint32) (io.ReadWriteCloser, error) { + return nil, errors.Newf(errors.Permission, "portforwarning not allowed") +} diff --git a/pkg/environment/facade-repository.go b/pkg/environment/facade-repository.go index dc96963..dda9081 100644 --- a/pkg/environment/facade-repository.go +++ b/pkg/environment/facade-repository.go @@ -5,6 +5,8 @@ import ( "fmt" "reflect" + "github.com/gliderlabs/ssh" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/configuration" "github.com/engity-com/bifroest/pkg/errors" @@ -41,6 +43,15 @@ func (this *RepositoryFacade) WillBeAccepted(req Request) (bool, error) { return candidate.WillBeAccepted(req) } +func (this *RepositoryFacade) DoesSupportPty(req Request, pty ssh.Pty) (bool, error) { + flow := req.Authorization().Flow() + candidate, ok := this.entries[flow] + if !ok { + return false, fmt.Errorf("does not find valid environment for flow %v", flow) + } + return candidate.DoesSupportPty(req, pty) +} + func (this *RepositoryFacade) Ensure(req Request) (Environment, error) { flow := req.Authorization().Flow() candidate, ok := this.entries[flow] diff --git a/pkg/environment/local-repository.go b/pkg/environment/local-repository.go index 6434fc2..04f8ec9 100644 --- a/pkg/environment/local-repository.go +++ b/pkg/environment/local-repository.go @@ -6,6 +6,7 @@ import ( "fmt" log "github.com/echocat/slf4g" + "github.com/gliderlabs/ssh" "github.com/engity-com/bifroest/pkg/configuration" "github.com/engity-com/bifroest/pkg/errors" @@ -65,6 +66,10 @@ func (this *LocalRepository) WillBeAccepted(req Request) (ok bool, err error) { return ok, nil } +func (this *LocalRepository) DoesSupportPty(Request, ssh.Pty) (bool, error) { + return true, nil +} + func (this *LocalRepository) Ensure(req Request) (Environment, error) { fail := func(err error) (Environment, error) { return nil, err diff --git a/pkg/environment/repository.go b/pkg/environment/repository.go index 0ebd509..9ff5a1b 100644 --- a/pkg/environment/repository.go +++ b/pkg/environment/repository.go @@ -6,6 +6,7 @@ import ( "io" log "github.com/echocat/slf4g" + "github.com/gliderlabs/ssh" "github.com/engity-com/bifroest/pkg/session" ) @@ -20,6 +21,10 @@ type Repository interface { // provided Request. WillBeAccepted(Request) (bool, error) + // DoesSupportPty will return true if the resulting Environment will support + // an PTY. + DoesSupportPty(Request, ssh.Pty) (bool, error) + // Ensure will create or return an environment that matches the given Request. // If it is not acceptable to do this action with the provided Request // ErrNotAcceptable is returned; you can call WillBeAccepted to prevent such diff --git a/pkg/service/service-authorization.go b/pkg/service/service-authorization.go index c5322b6..a8947b6 100644 --- a/pkg/service/service-authorization.go +++ b/pkg/service/service-authorization.go @@ -149,3 +149,29 @@ func (this *service) resolveAuthorizationAndSession(sshSess ssh.Session) (author } return auth, sess, oldState, nil } + +func (this *service) onPtyRequest(ctx ssh.Context, pty ssh.Pty) bool { + auth, ok := ctx.Value(authorizationCtxKey).(authorization.Authorization) + if !ok { + return false + } + + logger := this.logger(ctx) + + ok, err := this.environments.DoesSupportPty(&environmentRequest{ + this, + &remote{ctx}, + auth, + }, pty) + if this.isRelevantError(err) { + logger.WithError(err).Warn("cannot evaluate if PTY is allowed or not for request") + return false + } + + if !ok { + logger.Debug("PTY was requested but is forbidden") + } + + logger.Debug("PTY was requested and was permitted") + return true +} diff --git a/pkg/service/service_linux.go b/pkg/service/service_linux.go index 327dad3..88172fe 100644 --- a/pkg/service/service_linux.go +++ b/pkg/service/service_linux.go @@ -5,8 +5,6 @@ package service import ( "syscall" - "github.com/gliderlabs/ssh" - "github.com/engity-com/bifroest/pkg/errors" ) @@ -25,7 +23,3 @@ func (this *service) isAcceptableNewConnectionError(err error) bool { return false } - -func (this *service) onPtyRequest(_ ssh.Context, _ ssh.Pty) bool { - return true -} diff --git a/pkg/service/service_windows.go b/pkg/service/service_windows.go index a06a451..5ffe16f 100644 --- a/pkg/service/service_windows.go +++ b/pkg/service/service_windows.go @@ -5,8 +5,6 @@ package service import ( "syscall" - "github.com/gliderlabs/ssh" - "github.com/engity-com/bifroest/pkg/errors" ) @@ -27,7 +25,3 @@ func (this *service) isAcceptableNewConnectionError(err error) bool { return false } - -func (this *service) onPtyRequest(_ ssh.Context, _ ssh.Pty) bool { - return false -}