diff --git a/.vagrant/machines/default/libvirt/created_networks b/.vagrant/machines/default/libvirt/created_networks index 2e2c2cf..ded9d28 100644 --- a/.vagrant/machines/default/libvirt/created_networks +++ b/.vagrant/machines/default/libvirt/created_networks @@ -10,3 +10,4 @@ 5533801d-70e3-4c21-9942-82d20930c789 5533801d-70e3-4c21-9942-82d20930c789 5533801d-70e3-4c21-9942-82d20930c789 +5533801d-70e3-4c21-9942-82d20930c789 diff --git a/README.md b/README.md index 0771799..b96b6a4 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,18 @@ Users to use Webtop in their own, separate session. A good illustration is this: To build the project, you can just use "make" On Debian 12, you will need the following dependencies: -``` +```sh apt install -y git wget podman make gcc libgpgme-dev build-essential pkgconf pkgconf-bin libdevmapper-dev libbtrfs-dev ``` ## Installing Executing `make install` will install and start podterminal as a systemd service, including an example config file. -## Running -To run the built binary, just execute it as root. You will have to have Podman installed and its socket enabled. -Currently you have to set the Image, port etc. directly in the Source Code, however that should be eventually moved into -a config file. +## Usage +There is some basic Documentation in the config file to explain the usage of the keys. For more in-depth explanations +feel free to contact me, and I will add it to the documentation + +## Known Bugs +- Images need to be pulled first as root +- Not compatible with default KASM Docker images (needs http WSS access) +- reloading browser on startup might be necessary, especially if the target image is slow to start diff --git a/exampleconfig.yaml b/exampleconfig.yaml index 50c9e2d..408bee4 100644 --- a/exampleconfig.yaml +++ b/exampleconfig.yaml @@ -24,3 +24,5 @@ ssl_cert: /etc/ssl/certs/ssl-cert-snakeoil.pem ssl_cert_key: /etc/ssl/private/ssl-cert-snakeoil.key # Blocks accessing /files and /files/socket.io to avoid File transfer block_filebrowser: false +# Delete running containers when restarting podterminal +timeout_on_restart: true diff --git a/main.go b/main.go index 567c124..1da58d8 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,7 @@ func main() { log.Println("Dropped Privileges") g.Go(pods.GarbageCollector) g.Go(pods.PullImage) + g.Go(web.IdleSessionCleanup) // prevent main thread from dying if err := g.Wait(); err != nil { diff --git a/pods/container.go b/pods/container.go index e83039a..f409b6b 100644 --- a/pods/container.go +++ b/pods/container.go @@ -10,6 +10,8 @@ import ( "github.com/spf13/viper" ) +// CreateContainer connects to the Podman socket defined in the "Socket" variable in this scope +// and creates a new container using the Image defined in the config file func CreateContainer() (string, error) { image := viper.GetString("image") conn := Socket diff --git a/pods/garbageCollector.go b/pods/garbageCollector.go index d3d67c3..254938e 100644 --- a/pods/garbageCollector.go +++ b/pods/garbageCollector.go @@ -1,10 +1,18 @@ package pods import ( + "log" "time" + + "github.com/containers/podman/v4/pkg/bindings/volumes" + "github.com/spf13/viper" ) +// GarbageCollector is a goroutine that cleans up old Containers func GarbageCollector() error { + if viper.GetBool("timeout_on_restart") { + timeoutExistingContainers() + } for { err := Cleanup() if err != nil { @@ -13,3 +21,28 @@ func GarbageCollector() error { time.Sleep(time.Minute * 10) } } + +func timeoutExistingContainers() { + var oldContainers []string + + for _, container := range containerList() { + oldContainers = append(oldContainers, container.ID) + } + OldContainers = append(OldContainers, oldContainers...) + log.Println("old Containers: ", oldContainers) + +} + +func pruneVolumes() error { + + results, err := volumes.Prune(Socket, nil) + if err != nil { + return err + } + resultLen := len(results) + for i, result := range results { + log.Printf("[%d/%d] %s %d MB", i, resultLen, result.Id, result.Size/1024/1024) + } + return nil + +} diff --git a/pods/manager.go b/pods/manager.go index 6b939aa..affc024 100644 --- a/pods/manager.go +++ b/pods/manager.go @@ -3,7 +3,6 @@ package pods import ( "context" "log" - "net" "time" "github.com/containers/podman/v4/pkg/bindings" @@ -14,7 +13,6 @@ import ( ) var Socket context.Context -var rawSocket net.Conn var OldContainers []string @@ -27,19 +25,8 @@ func socketConnection() context.Context { return conn } -func rawConnection() net.Conn { - connection, err := net.Dial("unix", "unix:///run/podman/podman.sock") - if err != nil { - log.Println( - "Could not establish raw UNIX socket connection, certain features will not work properly", - ) - } - return connection -} - func ConnectSocket() { Socket = socketConnection() - rawSocket = rawConnection() } func PullImage() error { @@ -64,22 +51,21 @@ func Cleanup() error { containerList := containerList() for _, container := range containerList { - now := time.Now() - maxAge := time.Second * time.Duration(viper.GetInt("maxAge")) - containerAge := now.Sub(container.Created) - if containerAge > maxAge { - - err := containers.Kill(Socket, container.ID, nil) - if err != nil { - log.Println(err) - return err - } - _, err = containers.Remove(Socket, container.ID, nil) - if err != nil { - log.Println(err) - return err + for _, ctid := range OldContainers { + if container.ID == ctid { + err := containers.Kill(Socket, container.ID, nil) + if err != nil { + log.Println(err) + return err + } + _, err = containers.Remove(Socket, container.ID, nil) + if err != nil { + log.Println(err) + return err + } } } + } return nil } diff --git a/readConfig.go b/readConfig.go index 89b13f8..31727f0 100644 --- a/readConfig.go +++ b/readConfig.go @@ -26,6 +26,8 @@ func readConfigFile() { viper.SetDefault("block_filebrowser", false) viper.SetDefault("session_key", sessionKey) viper.SetDefault("container_port", 3000) + viper.SetDefault("container_protocol", "http") + viper.SetDefault("timeout_on_restart", true) viper.SetDefault("envvars", map[string]string{ "CUSTOM_USER": "user", diff --git a/web/reverseProxy.go b/web/reverseProxy.go index 13e8d75..02274dc 100644 --- a/web/reverseProxy.go +++ b/web/reverseProxy.go @@ -3,12 +3,14 @@ package web import ( "fmt" "log" + "net/http" "net/http/httputil" "net/url" "time" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" + "github.com/spf13/viper" "git.jmbit.de/jmb/podterminal/pods" ) @@ -27,6 +29,15 @@ func createReverseProxy(backendService string) (*httputil.ReverseProxy, error) { request.SetURL(backendURL) request.Out.Host = request.In.Host }, + ModifyResponse: func(response *http.Response) error { + if response.StatusCode == http.StatusBadGateway { + time.Sleep(time.Second) + response.StatusCode = 307 + response.Header.Set("Location", "/") + + } + return nil + }, } return proxy, err @@ -34,9 +45,10 @@ func createReverseProxy(backendService string) (*httputil.ReverseProxy, error) { func containerProxy(c *gin.Context) { session := sessions.Default(c) - session.Save() - sessionID := sessions.Session.ID(session) - if session.Get("ct") == nil { + sessionID := "" + if session.Get("ct") == nil && session.Get("ready") == nil { + session.Set("ready", false) + session.Save() log.Println("Creating Container for Session ", sessionID) ct, err := pods.CreateContainer() session.Set("ct", ct) @@ -67,7 +79,9 @@ func containerProxy(c *gin.Context) { // Soft fail Skel // _ = pods.CopySkelToContainer(ct) - proxies[ct], err = createReverseProxy(fmt.Sprintf("http://%s:3000", ctip)) + proxies[ct], err = createReverseProxy( + fmt.Sprintf("http://%s:%d", ctip, viper.GetInt("container_port")), + ) if err != nil { c.HTML( @@ -81,7 +95,7 @@ func containerProxy(c *gin.Context) { } session.Set("ready", true) session.Save() - c.Redirect(301, "/") + c.Redirect(307, "/") } else { sessionCT := session.Get("ct") switch sessionCT.(type) { @@ -90,16 +104,25 @@ func containerProxy(c *gin.Context) { default: c.HTML(500, "Error", "Session Container ID is not a string") session.Delete("ct") + session.Delete("ready") session.Save() c.Abort() } - if session.Get("ready") == nil { - time.Sleep(100 * time.Millisecond) - c.Redirect(301, "/") + if session.Get("ready").(bool) == false { + time.Sleep(time.Second) + c.Redirect(307, "/") } id := session.Get("ct").(string) proxy := proxies[id] - proxy.ServeHTTP(c.Writer, c.Request) + if proxy != nil { + proxy.ServeHTTP(c.Writer, c.Request) + } else { + session.Delete("ct") + session.Delete("ready") + session.Save() + c.Abort() + } + } } diff --git a/web/sessionAging.go b/web/sessionAging.go index e33e260..b76a7a5 100644 --- a/web/sessionAging.go +++ b/web/sessionAging.go @@ -1,6 +1,7 @@ package web import ( + "log" "time" "github.com/spf13/viper" @@ -33,9 +34,12 @@ func deleteIdleSessions() { tenMinutesAgo := -time.Duration(idleTimout) * time.Second oldAge := time.Now().Add(tenMinutesAgo) for session, sessionData := range sessionLastAccess { + log.Printf("Session %s last connected at %s", session, sessionData.lastAccess.String()) if oldAge.After(*sessionData.lastAccess) { pods.DestroyContainer(session) invalidSessions = append(invalidSessions, sessionData.sessionID) + // Delete Proxy entry to avoid 502s + delete(proxies, session) }