Compare commits
No commits in common. "37f2801d53ca47ab2cf3e70a1764f47d068f4e26" and "e5c78de08f9c5566008a7c4081a9934465496964" have entirely different histories.
37f2801d53
...
e5c78de08f
|
@ -10,4 +10,3 @@
|
|||
5533801d-70e3-4c21-9942-82d20930c789
|
||||
5533801d-70e3-4c21-9942-82d20930c789
|
||||
5533801d-70e3-4c21-9942-82d20930c789
|
||||
5533801d-70e3-4c21-9942-82d20930c789
|
||||
|
|
14
README.md
14
README.md
|
@ -8,18 +8,14 @@ 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.
|
||||
|
||||
## 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
|
||||
## 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.
|
||||
|
|
|
@ -11,7 +11,7 @@ envvars:
|
|||
# Container Image you want to use
|
||||
image: git.jmbit.de/jmb/webtop-plus:latest
|
||||
# Maximum age of Session
|
||||
maxage: 0
|
||||
maxage: 10800
|
||||
# Port Podterminal should listen to
|
||||
port: 80
|
||||
# Files that will be copied into container on startup
|
||||
|
@ -24,5 +24,3 @@ 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
|
||||
|
|
1
main.go
1
main.go
|
@ -25,7 +25,6 @@ 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 {
|
||||
|
|
|
@ -2,7 +2,6 @@ package pods
|
|||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/containers/podman/v4/pkg/bindings/containers"
|
||||
"github.com/containers/podman/v4/pkg/specgen"
|
||||
|
@ -10,8 +9,6 @@ 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
|
||||
|
@ -53,30 +50,15 @@ func DestroyContainer(id string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetContainerIP returns a
|
||||
func GetContainerIP(id string) (string, error) {
|
||||
conn := Socket
|
||||
var ip string
|
||||
var err error
|
||||
i := 0
|
||||
|
||||
for i < 50 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
container, err := containers.Inspect(conn, id, nil)
|
||||
if err != nil {
|
||||
log.Println("Could not get IP of container", err)
|
||||
return "", err
|
||||
}
|
||||
ip = container.NetworkSettings.IPAddress
|
||||
log.Println(ip)
|
||||
if len(ip) > 5 {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i > 50 {
|
||||
log.Println("timed out waiting for IP")
|
||||
container, err := containers.Inspect(conn, id, nil)
|
||||
if err != nil {
|
||||
log.Println("Could not get IP of container", err)
|
||||
return "", err
|
||||
}
|
||||
ip := container.NetworkSettings.IPAddress
|
||||
|
||||
return ip, err
|
||||
}
|
||||
|
|
|
@ -1,18 +1,10 @@
|
|||
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 {
|
||||
|
@ -21,28 +13,3 @@ 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
|
||||
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package pods
|
|||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/containers/podman/v4/pkg/bindings"
|
||||
|
@ -13,8 +14,7 @@ import (
|
|||
)
|
||||
|
||||
var Socket context.Context
|
||||
|
||||
var OldContainers []string
|
||||
var rawSocket net.Conn
|
||||
|
||||
func socketConnection() context.Context {
|
||||
uri := "unix:///run/podman/podman.sock"
|
||||
|
@ -25,8 +25,19 @@ 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 {
|
||||
|
@ -51,21 +62,22 @@ func Cleanup() error {
|
|||
containerList := containerList()
|
||||
|
||||
for _, container := range containerList {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -5,12 +5,9 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"git.jmbit.de/jmb/podterminal/utils"
|
||||
)
|
||||
|
||||
func readConfigFile() {
|
||||
sessionKey, _ := utils.RandomString(64)
|
||||
log.Println("Reading Config")
|
||||
viper.SetConfigFile("/etc/podterminal/config.yaml")
|
||||
viper.SetDefault("port", 80)
|
||||
|
@ -24,10 +21,6 @@ func readConfigFile() {
|
|||
viper.SetDefault("skel_target", "/")
|
||||
viper.SetDefault("skel_user", "")
|
||||
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",
|
||||
|
|
|
@ -3,14 +3,12 @@ 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"
|
||||
)
|
||||
|
@ -29,15 +27,6 @@ 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
|
||||
|
@ -45,43 +34,33 @@ func createReverseProxy(backendService string) (*httputil.ReverseProxy, error) {
|
|||
|
||||
func containerProxy(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
sessionID := ""
|
||||
if session.Get("ct") == nil && session.Get("ready") == nil {
|
||||
session.Set("ready", false)
|
||||
session.Save()
|
||||
session.Save()
|
||||
sessionID := session.ID()
|
||||
if session.Get("ct") == nil {
|
||||
log.Println("Creating Container for Session ", sessionID)
|
||||
ct, err := pods.CreateContainer()
|
||||
session.Set("ct", ct)
|
||||
session.Save()
|
||||
if err != nil {
|
||||
c.HTML(500, "Error", fmt.Sprintf("[%s] Could not create Container: %v", sessionID, err))
|
||||
session.Delete("ct")
|
||||
session.Save()
|
||||
c.Abort()
|
||||
}
|
||||
err = pods.StartContainer(ct)
|
||||
if err != nil {
|
||||
c.HTML(500, "Error", fmt.Sprintf("[%s] Could not start Container: %v", sessionID, err))
|
||||
session.Delete("ct")
|
||||
session.Save()
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
// Hack to wait for Container to start up and get assigned an IP
|
||||
time.Sleep(3 * time.Second)
|
||||
ctip, err := pods.GetContainerIP(ct)
|
||||
|
||||
if err != nil {
|
||||
c.HTML(500, "Error", fmt.Sprintf("[%s] Could not get Container ip: %v", sessionID, err))
|
||||
session.Delete("ct")
|
||||
session.Save()
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
// Soft fail Skel
|
||||
// _ = pods.CopySkelToContainer(ct)
|
||||
|
||||
proxies[ct], err = createReverseProxy(
|
||||
fmt.Sprintf("http://%s:%d", ctip, viper.GetInt("container_port")),
|
||||
)
|
||||
proxies[ct], err = createReverseProxy(fmt.Sprintf("http://%s:3000", ctip))
|
||||
|
||||
if err != nil {
|
||||
c.HTML(
|
||||
|
@ -89,13 +68,11 @@ func containerProxy(c *gin.Context) {
|
|||
"Error",
|
||||
fmt.Sprintf("[%s] Could not create Container Proxy: %v", sessionID, err),
|
||||
)
|
||||
session.Delete("ct")
|
||||
session.Save()
|
||||
c.Abort()
|
||||
}
|
||||
session.Set("ready", true)
|
||||
session.Set("ct", ct)
|
||||
session.Save()
|
||||
c.Redirect(307, "/")
|
||||
c.Redirect(301, "/")
|
||||
} else {
|
||||
sessionCT := session.Get("ct")
|
||||
switch sessionCT.(type) {
|
||||
|
@ -103,26 +80,11 @@ 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").(bool) == false {
|
||||
time.Sleep(time.Second)
|
||||
c.Redirect(307, "/")
|
||||
}
|
||||
id := session.Get("ct").(string)
|
||||
proxy := proxies[id]
|
||||
if proxy != nil {
|
||||
proxy.ServeHTTP(c.Writer, c.Request)
|
||||
} else {
|
||||
session.Delete("ct")
|
||||
session.Delete("ready")
|
||||
session.Save()
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
|
@ -41,7 +42,7 @@ func setupRouter() *gin.Engine {
|
|||
gin.ForceConsoleColor()
|
||||
gin.SetMode("release")
|
||||
router := gin.New()
|
||||
store := cookie.NewStore([]byte(viper.GetString("session_key")))
|
||||
store := cookie.NewStore([]byte(uuid.NewString()))
|
||||
store.Options(sessions.Options{
|
||||
MaxAge: viper.GetInt("maxAge"),
|
||||
})
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"git.jmbit.de/jmb/podterminal/pods"
|
||||
)
|
||||
|
||||
type sessionData struct {
|
||||
lastAccess *time.Time
|
||||
sessionID string
|
||||
}
|
||||
|
||||
var invalidSessions []string
|
||||
var sessionLastAccess map[string]*sessionData
|
||||
|
||||
func initSessionAging() error {
|
||||
sessionLastAccess = make(map[string]*sessionData)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateSession(id string, sessionID string) {
|
||||
nowTime := time.Now()
|
||||
sessionLastAccess[id].lastAccess = &nowTime
|
||||
sessionLastAccess[id].sessionID = sessionID
|
||||
}
|
||||
|
||||
func deleteIdleSessions() {
|
||||
idleTimout := viper.GetInt("session_timeout")
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func IdleSessionCleanup() error {
|
||||
err := initSessionAging()
|
||||
if err != nil {
|
||||
println("Could not initialize Session aging")
|
||||
return err
|
||||
}
|
||||
for {
|
||||
deleteIdleSessions()
|
||||
time.Sleep(time.Duration(viper.GetInt("session_timeout")) * time.Second)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue