diff --git a/cmd/ssh.go b/cmd/ssh.go new file mode 100644 index 0000000..76c6e4c --- /dev/null +++ b/cmd/ssh.go @@ -0,0 +1,54 @@ +package cmd + +import ( + wisski_distillery "github.com/FAU-CDI/wisski-distillery" + "github.com/FAU-CDI/wisski-distillery/internal/cli" + "github.com/tkw1536/goprogram/exit" +) + +// SSH is the 'ssh' command +var SSH wisski_distillery.Command = ssh{} + +type ssh struct { + Bind string `short:"b" long:"bind" description:"address to listen on" default:"127.0.0.1:2223"` +} + +func (s ssh) Description() wisski_distillery.Description { + return wisski_distillery.Description{ + Requirements: cli.Requirements{ + NeedsDistillery: true, + }, + Command: "ssh", + Description: "Starts the ssh server to allow clients to connect to this distillery", + } +} + +var errSSHListen = exit.Error{ + ExitCode: exit.ExitGeneric, + Message: "Unable to listen", +} + +func (s ssh) Run(context wisski_distillery.Context) error { + dis := context.Environment + server, err := dis.SSH().Server(dis.Context(), context.IOStream) + if err != nil { + return err + } + + context.Printf("Listening on %s\n", s.Bind) + + // make a new listener + listener, err := dis.Still.Environment.Listen("tcp", s.Bind) + if err != nil { + return errServerListen.Wrap(err) + } + + go func() { + <-dis.Context().Done() + listener.Close() + }() + + // and serve that listener + err = server.Serve(listener) + return errServerListen.Wrap(err) +} diff --git a/cmd/wdcli/main.go b/cmd/wdcli/main.go index b8e4e32..b9d282b 100644 --- a/cmd/wdcli/main.go +++ b/cmd/wdcli/main.go @@ -56,6 +56,7 @@ func init() { // servers wdcli.Register(cmd.Server) + wdcli.Register(cmd.SSH) } // an error when no arguments are provided. diff --git a/go.mod b/go.mod index b89da52..cbf47cc 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,12 @@ require ( github.com/Showmax/go-fqdn v1.0.0 github.com/alessio/shellescape v1.4.1 github.com/feiin/sqlstring v0.3.0 + github.com/gliderlabs/ssh v0.3.5 github.com/go-sql-driver/mysql v1.6.0 github.com/gorilla/websocket v1.5.0 github.com/pkg/errors v0.9.1 github.com/tkw1536/goprogram v0.1.1 + github.com/tkw1536/proxyssh v0.2.2 golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741 golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 gorm.io/driver/mysql v1.3.6 @@ -18,11 +20,13 @@ require ( ) require ( + github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/gosuri/uilive v0.0.4 // indirect github.com/jessevdk/go-flags v1.5.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-isatty v0.0.16 // indirect + golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d // indirect golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect ) diff --git a/go.sum b/go.sum index 97801d3..3310a2b 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,12 @@ github.com/Showmax/go-fqdn v1.0.0 h1:0rG5IbmVliNT5O19Mfuvna9LL7zlHyRfsSvBPZmF9tM github.com/Showmax/go-fqdn v1.0.0/go.mod h1:SfrFBzmDCtCGrnHhoDjuvFnKsWjEQX/Q9ARZvOrJAko= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/feiin/sqlstring v0.3.0 h1:iyPEFijI2BxpY2M+AuhIvdNManzXa2OwGzuPaEMLUgo= github.com/feiin/sqlstring v0.3.0/go.mod h1:xpZTjVUw1nD3hMgF9SMRdPiooKSikLf4PS5j2NTn3RI= +github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= @@ -25,16 +29,33 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/tkw1536/goprogram v0.1.1 h1:gamK9OuRqoX2yQlA/nkgfVHHZWd/u2uUj6vJMYrYa70= github.com/tkw1536/goprogram v0.1.1/go.mod h1:Jqs0sTMzhrAGCX3JQrlEwQ0WRWQACCvuQQkaBDp65pE= +github.com/tkw1536/proxyssh v0.2.2 h1:2NVlTsRFFVfGaQH6B0Ci8n0ura1Qk4uEcZM0cDXAIF4= +github.com/tkw1536/proxyssh v0.2.2/go.mod h1:N6fExNESwKnKV1d0JGT3fvke+Aym22VNCAPEGJTofPQ= +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/exp v0.0.0-20221004215720-b9f4876ce741 h1:fGZugkZk2UgYBxtpKmvub51Yno1LJDeEsRp2xGD+0gY= golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-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.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/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/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w= golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gorm.io/driver/mysql v1.3.6 h1:BhX1Y/RyALb+T9bZ3t07wLnPZBukt+IRkMn8UZSNbGM= gorm.io/driver/mysql v1.3.6/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= diff --git a/internal/dis/component/ssh/ssh.go b/internal/dis/component/ssh/ssh.go new file mode 100644 index 0000000..7770c8f --- /dev/null +++ b/internal/dis/component/ssh/ssh.go @@ -0,0 +1,104 @@ +package ssh + +import ( + "bufio" + "context" + "fmt" + "io" + + "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" + "github.com/FAU-CDI/wisski-distillery/pkg/sshx" + "github.com/gliderlabs/ssh" + "github.com/tkw1536/goprogram/stream" + "github.com/tkw1536/proxyssh/feature" +) + +type SSH struct { + component.Base + Instances *instances.Instances +} + +const ( + etx rune = 3 + eot rune = 4 +) + +const welcomeMessage = `Welcome to the WissKI SSH Server. +You've successfully authenticated, but we don't provide shell access to the main server. +You may use this connection as part of a proxy jump to connect to your server. +For example: + +ssh -J %s:2222 www-data@%s + +Press CTRL-C to close this connection. +` + +// Server returns an ssh server that implements the main ssh server +func (s *SSH) Server(context context.Context, ios stream.IOStream) (*ssh.Server, error) { + var server ssh.Server + + banner := fmt.Sprintf(welcomeMessage, s.Config.DefaultDomain, "example."+s.Config.DefaultDomain) + + server.Handle(func(session ssh.Session) { + io.WriteString(session, banner) + + buffer := bufio.NewReader(session) + for { + res, _, err := buffer.ReadRune() + if err != nil { + return + } + if res == etx || res == eot { + return + } + } + + }) + server.PublicKeyHandler = feature.AuthorizeKeys( + slogger{IOStream: ios}, + func(ctx ssh.Context) (keys []ssh.PublicKey, err error) { + keys, err = s.GlobalKeys() + if err != nil { + return nil, err + } + + instances, err := s.Instances.All() + if err != nil { + return nil, err + } + + for _, instance := range instances { + ikeys, err := instance.SSH().Keys() + if err != nil { + continue + } + keys = append(keys, ikeys...) + } + return keys, nil + }) + return &server, nil +} + +func (s *SSH) GlobalKeys() ([]ssh.PublicKey, error) { + file, err := s.Environment.Open(s.Config.GlobalAuthorizedKeysFile) + if err != nil { + return nil, err + } + bytes, err := io.ReadAll(file) + if err != nil { + return nil, err + } + return sshx.ParseAllKeys(bytes), nil +} + +type slogger struct { + stream.IOStream +} + +func (s slogger) Print(v ...any) { + fmt.Fprint(s.Stderr, v...) +} +func (s slogger) Printf(format string, v ...any) { + fmt.Fprintf(s.Stderr, format, v...) +} diff --git a/internal/dis/distillery.go b/internal/dis/distillery.go index c5adce3..f435be5 100644 --- a/internal/dis/distillery.go +++ b/internal/dis/distillery.go @@ -20,6 +20,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component/resolver" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/solr" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/triplestore" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/web" "github.com/FAU-CDI/wisski-distillery/pkg/lazy" @@ -72,6 +73,10 @@ func (dis *Distillery) Resolver() *resolver.Resolver { func (dis *Distillery) SQL() *sql.SQL { return export[*sql.SQL](dis) } +func (dis *Distillery) SSH() *ssh.SSH { + return export[*ssh.SSH](dis) +} + func (dis *Distillery) Triplestore() *triplestore.Triplestore { return export[*triplestore.Triplestore](dis) } @@ -119,7 +124,7 @@ func (dis *Distillery) allComponents() []initFunc { s.PollInterval = time.Second }), - // instainces + // instances auto[*instances.Instances], auto[*meta.Meta], auto[*malt.Malt], @@ -132,6 +137,9 @@ func (dis *Distillery) allComponents() []initFunc { auto[*exporter.Filesystem], auto[*exporter.Pathbuilders], + // ssh server + auto[*ssh.SSH], + // Control server auto[*control.Control], auto[*static.Static], diff --git a/internal/wisski/ingredient/barrel/barrel.go b/internal/wisski/ingredient/barrel/barrel.go index b2e2f86..7afe4b3 100644 --- a/internal/wisski/ingredient/barrel/barrel.go +++ b/internal/wisski/ingredient/barrel/barrel.go @@ -1,6 +1,8 @@ package barrel import ( + "path/filepath" + "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/locker" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/mstore" @@ -13,3 +15,11 @@ type Barrel struct { Locker *locker.Locker MStore *mstore.MStore } + +func (barrel *Barrel) DataPath() string { + return filepath.Join(barrel.FilesystemBase, "data") +} + +func (barrel *Barrel) AuthorizedKeysPath() string { + return filepath.Join(barrel.DataPath(), "authorized_keys") +} diff --git a/internal/wisski/ingredient/barrel/ssh/ssh.go b/internal/wisski/ingredient/barrel/ssh/ssh.go new file mode 100644 index 0000000..bc2b2f8 --- /dev/null +++ b/internal/wisski/ingredient/barrel/ssh/ssh.go @@ -0,0 +1,32 @@ +package ssh + +import ( + "io" + + "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" + "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel" + "github.com/FAU-CDI/wisski-distillery/pkg/environment" + "github.com/FAU-CDI/wisski-distillery/pkg/sshx" + "github.com/gliderlabs/ssh" +) + +type SSH struct { + ingredient.Base + Barrel *barrel.Barrel +} + +func (ssh *SSH) Keys() ([]ssh.PublicKey, error) { + file, err := ssh.Environment.Open(ssh.Barrel.AuthorizedKeysPath()) + if environment.IsNotExist(err) { + return nil, nil + } + if err != nil { + return nil, err + } + + bytes, err := io.ReadAll(file) + if err != nil { + return nil, err + } + return sshx.ParseAllKeys(bytes), nil +} diff --git a/internal/wisski/wisski.go b/internal/wisski/wisski.go index 48ecda4..af39a37 100644 --- a/internal/wisski/wisski.go +++ b/internal/wisski/wisski.go @@ -8,6 +8,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/drush" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/provisioner" + "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/ssh" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/bookkeeping" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/info" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/locker" @@ -76,6 +77,10 @@ func (wisski *WissKI) Info() *info.Info { return export[*info.Info](wisski) } +func (wisski *WissKI) SSH() *ssh.SSH { + return export[*ssh.SSH](wisski) +} + // // All components // THESE SHOULD NEVER BE CALLED DIRECTLY @@ -112,5 +117,7 @@ func (wisski *WissKI) allIngredients() []initFunc { auto[*drush.Drush], auto[*reserve.Reserve], + + auto[*ssh.SSH], } } diff --git a/pkg/sshx/sshx.go b/pkg/sshx/sshx.go new file mode 100644 index 0000000..bf02389 --- /dev/null +++ b/pkg/sshx/sshx.go @@ -0,0 +1,17 @@ +package sshx + +import "github.com/gliderlabs/ssh" + +// ParseAllKeys parses all keys from the list of bytes +func ParseAllKeys(bytes []byte) (keys []ssh.PublicKey) { + var key ssh.PublicKey + var err error + for { + key, _, _, bytes, err = ssh.ParseAuthorizedKey(bytes) + if err != nil { + break + } + keys = append(keys, key) + } + return +}