From 85c63f24a9d0fed8c4f5280342802e6d83f8c4f2 Mon Sep 17 00:00:00 2001 From: Tom Wiesing Date: Wed, 12 Apr 2023 13:42:27 +0200 Subject: [PATCH] ssh: Update help page and allow service forwarding --- internal/dis/component/auth/panel/panel.go | 2 + internal/dis/component/auth/panel/ssh.go | 6 ++ .../component/auth/panel/templates/ssh.html | 47 +++++++++++- internal/dis/component/component.go | 33 ++++++++- internal/dis/component/ssh2/server_forward.go | 72 ++++++++++++++++--- internal/dis/component/ssh2/ssh2.go | 3 + internal/dis/component/ssh2/stack.go | 4 +- internal/dis/distillery.go | 20 +----- internal/dis/init.go | 20 +++--- 9 files changed, 166 insertions(+), 41 deletions(-) diff --git a/internal/dis/component/auth/panel/panel.go b/internal/dis/component/auth/panel/panel.go index d90995d..ff076e4 100644 --- a/internal/dis/component/auth/panel/panel.go +++ b/internal/dis/component/auth/panel/panel.go @@ -10,6 +10,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2/sshkeys" "github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/julienschmidt/httprouter" @@ -25,6 +26,7 @@ type UserPanel struct { Instances *instances.Instances Next *next.Next Keys *sshkeys.SSHKeys + SSH2 *ssh2.SSH2 } } diff --git a/internal/dis/component/auth/panel/ssh.go b/internal/dis/component/auth/panel/ssh.go index 6f5ffc0..1ea3e7a 100644 --- a/internal/dis/component/auth/panel/ssh.go +++ b/internal/dis/component/auth/panel/ssh.go @@ -8,6 +8,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2" "github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/gliderlabs/ssh" "github.com/rs/zerolog" @@ -38,6 +39,9 @@ type SSHTemplateContext struct { Slug string // slug of the wisski Hostname string // hostname of an example wisski + + // Services are the special services reachable via ssh + Services []ssh2.Intercept } func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler { @@ -75,6 +79,8 @@ func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler { return sc, err } + sc.Services = panel.Dependencies.SSH2.Intercepts() + return sc, nil }) } diff --git a/internal/dis/component/auth/panel/templates/ssh.html b/internal/dis/component/auth/panel/templates/ssh.html index 219ee29..e86a82c 100644 --- a/internal/dis/component/auth/panel/templates/ssh.html +++ b/internal/dis/component/auth/panel/templates/ssh.html @@ -5,6 +5,7 @@
+

My SSH Keys

This table shows ssh keys currently associated with your account. To add a new key, use the Add New Key button above. @@ -57,8 +58,8 @@

- -
+
+

Configuring SSH Access

You can use these ssh keys to connect to the distillery via ssh. You can only connect to instances for which you appear as an Administrator on your user page. @@ -95,4 +96,46 @@ Host {{ .Domain }}.proxy ssh {{ .Hostname }}

+ +
+

Accessing Services

+ +

+ You can access the services powering the distillery if you have ssh access. + To access a service, simply use ssh port forwarding, and then access them under localhost. + Port forwarding is accomplished by appending -L localport:service:serverport to your ssh command line. +

+

+ For example to connect to the triplestore, you can use: +

+ +
+ssh -p {{ .Port }} {{ .Domain }} -L 7200:triplestore:7200
+
+
+

+ and then go to http://127.0.0.1:7200/. +

+ +

+ Note that you can add port forwards both when connecting to the top-level distillery ssh server as well as any instance - the syntax is identical. + You can also add multiple forwards at the same time, by adding multiple -L arguments. +

+ +

+ The complete list of services you can access are: +

    + {{ range .Services }} +
  • + {{ .Description }}, use -L {{.ExamplePort}}:{{ .Match.Host }}:{{.Match.Port}} and access at 127.0.0.1:{{.ExamplePort}} +
  • + {{ end }} +
+

+ +

+ Depending on the service you may need an additional password. + Distillery administrators can reveal these passwords in the admin interface. + Furthermore instance-specific passwords can typically be found in the Drupal Configuration, system-wide passwords in the Distillery Configuration. +

diff --git a/internal/dis/component/component.go b/internal/dis/component/component.go index 155e82c..9c993be 100644 --- a/internal/dis/component/component.go +++ b/internal/dis/component/component.go @@ -2,7 +2,9 @@ package component import ( + "net" "reflect" + "strconv" "strings" "github.com/FAU-CDI/wisski-distillery/internal/config" @@ -63,5 +65,34 @@ func (cb Base) ID() string { // Still represents the central part of a distillery. // It is used inside the main distillery struct, as well as every component via [ComponentBase]. type Still struct { - Config *config.Config // the configuration of the distillery + Config *config.Config // the configuration of the distillery + Upstream Upstream +} + +// Upstream contains the configuration for accessing remote configuration. +type Upstream struct { + SQL HostPort + Triplestore HostPort + Solr HostPort +} + +func (us Upstream) SQLAddr() string { + return us.SQL.String() +} + +func (us Upstream) TriplestoreAddr() string { + return us.Triplestore.String() +} + +func (us Upstream) SolrAddr() string { + return us.Solr.String() +} + +type HostPort struct { + Host string + Port uint32 +} + +func (hp HostPort) String() string { + return net.JoinHostPort(hp.Host, strconv.Itoa(int(hp.Port))) } diff --git a/internal/dis/component/ssh2/server_forward.go b/internal/dis/component/ssh2/server_forward.go index 285acfd..3bf0117 100644 --- a/internal/dis/component/ssh2/server_forward.go +++ b/internal/dis/component/ssh2/server_forward.go @@ -1,9 +1,11 @@ package ssh2 import ( + "fmt" "io" "net" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/gliderlabs/ssh" gossh "golang.org/x/crypto/ssh" ) @@ -17,7 +19,7 @@ type localForwardChannelData struct { OriginPort uint32 } -// seetupForwardHandler sets up the forwarding handler for the ssh server +// setupForwardHandler sets up the forwarding handler for the ssh server func (ssh2 *SSH2) setupForwardHandler(server *ssh.Server) { if server.ChannelHandlers == nil { server.ChannelHandlers = make(map[string]ssh.ChannelHandler) @@ -28,6 +30,62 @@ func (ssh2 *SSH2) setupForwardHandler(server *ssh.Server) { server.ChannelHandlers["direct-tcpip"] = ssh2.handleDirectTCP } +type Intercept struct { + Description string + Match component.HostPort + Dest component.HostPort +} + +// ExamplePort returns a local port that can be forwarded to without root rights +func (i Intercept) ExamplePort() uint32 { + if i.Match.Port < 100 { + return i.Match.Port * 101 + } + if i.Match.Port < 1024 { + return i.Match.Port * 1001 + } + return i.Match.Port +} +func (i Intercept) Intercept(req component.HostPort) (intercepted bool, ok bool, dest component.HostPort, rejectReason string) { + if req.Host != i.Match.Host { + return false, ok, dest, rejectReason + } + + if req.Port != i.Match.Port { + return true, false, dest, fmt.Sprintf("%s listens on port %d", i.Description, i.Match.Port) + } + return true, true, i.Dest, "" +} + +func (ssh2 *SSH2) Intercepts() []Intercept { + return ssh2.interceptsC.Get(func() []Intercept { + return []Intercept{ + {Description: "Triplestore", Match: component.HostPort{Host: "triplestore", Port: 7200}, Dest: ssh2.Upstream.Triplestore}, + {Description: "SQL", Match: component.HostPort{Host: "sql", Port: 3306}, Dest: ssh2.Upstream.SQL}, + {Description: "PHPMyAdmin", Match: component.HostPort{Host: "phpmyadmin", Port: 80}, Dest: component.HostPort{Host: "phpmyadmin", Port: 80}}, + } + }) +} + +func (ssh2 *SSH2) getForwardDest(req component.HostPort, ctx ssh.Context) (ok bool, dest component.HostPort, rejectReason string) { + // check all the intercepts first + for _, i := range ssh2.Intercepts() { + intercepted, ok, dest, rejectReason := i.Intercept(req) + if !intercepted { + continue + } + return ok, dest, rejectReason + } + + // then check the instances + slug, ok := ssh2.Config.HTTP.SlugFromHost(req.Host) + if !ok || req.Port != 22 || !hasPermission(ctx, slug) { + return false, dest, "permission denied" + } + + return true, component.HostPort{Host: slug + "." + ssh2.Config.HTTP.PrimaryDomain + ".wisski", Port: 22}, "" +} + // handleDirectTCP handles a direct tcp connection for the server func (ssh2 *SSH2) handleDirectTCP(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) { d := localForwardChannelData{} @@ -36,18 +94,14 @@ func (ssh2 *SSH2) handleDirectTCP(srv *ssh.Server, conn *gossh.ServerConn, newCh return } - slug, ok := ssh2.Config.HTTP.SlugFromHost(d.DestAddr) - if !ok || d.DestPort != 22 || !hasPermission(ctx, slug) { - newChan.Reject(gossh.Prohibited, "permission denied") + ok, dest, rejectReason := ssh2.getForwardDest(component.HostPort{Host: d.DestAddr, Port: d.DestPort}, ctx) + if !ok { + newChan.Reject(gossh.Prohibited, rejectReason) return } - // TODO: move this into an instance function somewhere - // NOTE(twiesing): This should be moved - dest := net.JoinHostPort(slug+"."+ssh2.Config.HTTP.PrimaryDomain+".wisski", "22") - var dialer net.Dialer - dconn, err := dialer.DialContext(ctx, "tcp", dest) + dconn, err := dialer.DialContext(ctx, "tcp", dest.String()) if err != nil { newChan.Reject(gossh.ConnectionFailed, err.Error()) return diff --git a/internal/dis/component/ssh2/ssh2.go b/internal/dis/component/ssh2/ssh2.go index fa93c90..94e6a99 100644 --- a/internal/dis/component/ssh2/ssh2.go +++ b/internal/dis/component/ssh2/ssh2.go @@ -6,6 +6,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2/sshkeys" + "github.com/tkw1536/pkglib/lazy" ) type SSH2 struct { @@ -16,6 +17,8 @@ type SSH2 struct { Auth *auth.Auth Keys *sshkeys.SSHKeys } + + interceptsC lazy.Lazy[[]Intercept] } var ( diff --git a/internal/dis/component/ssh2/stack.go b/internal/dis/component/ssh2/stack.go index fa7b82a..d1601b0 100644 --- a/internal/dis/component/ssh2/stack.go +++ b/internal/dis/component/ssh2/stack.go @@ -8,7 +8,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component" ) -func (ssh SSH2) Path() string { +func (ssh *SSH2) Path() string { return filepath.Join(ssh.Still.Config.Paths.Root, "core", "ssh2") } @@ -36,7 +36,7 @@ func (ssh *SSH2) Stack() component.StackWithResources { }) } -func (ssh SSH2) Context(parent component.InstallationContext) component.InstallationContext { +func (ssh *SSH2) Context(parent component.InstallationContext) component.InstallationContext { return component.InstallationContext{ bootstrap.Executable: ssh.Config.Paths.CurrentExecutable(), // TODO: Does this make sense? } diff --git a/internal/dis/distillery.go b/internal/dis/distillery.go index de85e34..8838e2c 100644 --- a/internal/dis/distillery.go +++ b/internal/dis/distillery.go @@ -50,25 +50,11 @@ type Distillery struct { // Where interactive progress is displayed Progress io.Writer - // Upstream holds information to connect to the various running - // distillery components. - // - // NOTE(twiesing): This is intended to eventually allow full remote management of the distillery. - // But for now this will just hold upstream configuration. - Upstream Upstream - // lifetime holds all components lifetime lifetime.Lifetime[component.Component, component.Still] lifetimeInit sync.Once } -// Upstream contains the configuration for accessing remote configuration. -type Upstream struct { - SQL string - Triplestore string - Solr string -} - // // PUBLIC COMPONENT GETTERS // @@ -136,19 +122,19 @@ func (dis *Distillery) allComponents() []initFunc { auto[*web.Web], manual(func(ts *triplestore.Triplestore) { - ts.BaseURL = "http://" + dis.Upstream.Triplestore + ts.BaseURL = "http://" + dis.Upstream.TriplestoreAddr() ts.PollInterval = time.Second }), manual(func(sql *sql.SQL) { - sql.ServerURL = dis.Upstream.SQL + sql.ServerURL = dis.Upstream.SQLAddr() sql.PollInterval = time.Second }), auto[*sql.LockTable], auto[*sql.InstanceTable], manual(func(s *solr.Solr) { - s.BaseURL = dis.Upstream.Solr + s.BaseURL = dis.Upstream.SolrAddr() s.PollInterval = time.Second }), diff --git a/internal/dis/init.go b/internal/dis/init.go index 35996f2..8b1dcee 100644 --- a/internal/dis/init.go +++ b/internal/dis/init.go @@ -5,6 +5,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/cli" "github.com/FAU-CDI/wisski-distillery/internal/config" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/tkw1536/goprogram/exit" ) @@ -20,22 +21,21 @@ var errOpenConfig = exit.Error{ // NewDistillery creates a new distillery from the provided flags func NewDistillery(params cli.Params, flags cli.Flags, req cli.Requirements) (dis *Distillery, err error) { - dis = &Distillery{ - Upstream: Upstream{ - SQL: "127.0.0.1:3306", - Triplestore: "127.0.0.1:7200", - Solr: "127.0.0.1:8983", - }, + dis = new(Distillery) + dis.Still.Upstream = component.Upstream{ + SQL: component.HostPort{Host: "127.0.0.1", Port: 3306}, + Triplestore: component.HostPort{Host: "127.0.0.1", Port: 7200}, + Solr: component.HostPort{Host: "127.0.0.1", Port: 8983}, } // we are within the docker // - // so setup the ports to connect everything to peroperly. + // so setup the ports to connect everything to properly. // also override some of the parameters for the environment. if flags.InternalInDocker { - dis.Upstream.SQL = "sql:3306" - dis.Upstream.Triplestore = "triplestore:7200" - dis.Upstream.Solr = "solr:8983" + dis.Still.Upstream.SQL = component.HostPort{Host: "sql", Port: 3306} + dis.Still.Upstream.Triplestore = component.HostPort{Host: "triplestore", Port: 7200} + dis.Still.Upstream.Solr = component.HostPort{Host: "solr", Port: 8983} params.ConfigPath = os.Getenv("CONFIG_PATH") }