Compare commits
5 Commits
Author | SHA1 | Date |
---|---|---|
Johannes Bülow | d12722cb6b | |
Johannes Bülow | 37f2801d53 | |
Johannes Bülow | 7ff1ec233d | |
Johannes Bülow | e5c78de08f | |
Johannes Bülow | 0e21438cc8 |
|
@ -8,3 +8,7 @@
|
||||||
5533801d-70e3-4c21-9942-82d20930c789
|
5533801d-70e3-4c21-9942-82d20930c789
|
||||||
5533801d-70e3-4c21-9942-82d20930c789
|
5533801d-70e3-4c21-9942-82d20930c789
|
||||||
5533801d-70e3-4c21-9942-82d20930c789
|
5533801d-70e3-4c21-9942-82d20930c789
|
||||||
|
5533801d-70e3-4c21-9942-82d20930c789
|
||||||
|
5533801d-70e3-4c21-9942-82d20930c789
|
||||||
|
5533801d-70e3-4c21-9942-82d20930c789
|
||||||
|
5533801d-70e3-4c21-9942-82d20930c789
|
||||||
|
|
17
README.md
17
README.md
|
@ -8,11 +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"
|
To build the project, you can just use "make"
|
||||||
|
|
||||||
On Debian 12, you will need the following dependencies:
|
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
|
apt install -y git wget podman make gcc libgpgme-dev build-essential pkgconf pkgconf-bin libdevmapper-dev libbtrfs-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running
|
## Installing
|
||||||
To run the built binary, just execute it as root. You will have to have Podman installed and its socket enabled.
|
Executing `make install` will install and start podterminal as a systemd service, including an example config file.
|
||||||
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
|
||||||
|
|
|
@ -9,19 +9,20 @@ envvars:
|
||||||
# HTTP_PROXY: 192.168.0.10
|
# HTTP_PROXY: 192.168.0.10
|
||||||
|
|
||||||
# Container Image you want to use
|
# Container Image you want to use
|
||||||
image: lscr.io/linuxserver/webtop
|
image: git.jmbit.de/jmb/webtop-plus:latest
|
||||||
# Maximum age of Session
|
# Maximum age of Session
|
||||||
maxage: 10800
|
maxage: 0
|
||||||
# Port Podterminal should listen to
|
# Port Podterminal should listen to
|
||||||
port: 80
|
port: 80
|
||||||
# Files that will be copied into container on startup
|
# Files that will be copied into container on startup
|
||||||
skel: /etc/podterminal/skel.tar.gz
|
skel: /etc/podterminal/skel.tar.gz
|
||||||
skel_target: /config
|
skel_target: /config
|
||||||
skel_chown: true
|
# Overrides UID/GID from tar archive to container user
|
||||||
# Currently useless
|
skel_chown: false
|
||||||
skel_owner: "abc"
|
|
||||||
ssl: false
|
ssl: false
|
||||||
ssl_cert: /etc/ssl/certs/ssl-cert-snakeoil.pem
|
ssl_cert: /etc/ssl/certs/ssl-cert-snakeoil.pem
|
||||||
ssl_cert_key: /etc/ssl/private/ssl-cert-snakeoil.key
|
ssl_cert_key: /etc/ssl/private/ssl-cert-snakeoil.key
|
||||||
# Blocks accessing /files and /files/socket.io to avoid File transfer
|
# Blocks accessing /files and /files/socket.io to avoid File transfer
|
||||||
block_filebrowser: false
|
block_filebrowser: false
|
||||||
|
# Delete running containers when restarting podterminal
|
||||||
|
timeout_on_restart: true
|
||||||
|
|
1
main.go
1
main.go
|
@ -25,6 +25,7 @@ func main() {
|
||||||
log.Println("Dropped Privileges")
|
log.Println("Dropped Privileges")
|
||||||
g.Go(pods.GarbageCollector)
|
g.Go(pods.GarbageCollector)
|
||||||
g.Go(pods.PullImage)
|
g.Go(pods.PullImage)
|
||||||
|
g.Go(web.IdleSessionCleanup)
|
||||||
|
|
||||||
// prevent main thread from dying
|
// prevent main thread from dying
|
||||||
if err := g.Wait(); err != nil {
|
if err := g.Wait(); err != nil {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package pods
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/containers/podman/v4/pkg/bindings/containers"
|
"github.com/containers/podman/v4/pkg/bindings/containers"
|
||||||
"github.com/containers/podman/v4/pkg/specgen"
|
"github.com/containers/podman/v4/pkg/specgen"
|
||||||
|
@ -9,6 +10,8 @@ import (
|
||||||
"github.com/spf13/viper"
|
"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) {
|
func CreateContainer() (string, error) {
|
||||||
image := viper.GetString("image")
|
image := viper.GetString("image")
|
||||||
conn := Socket
|
conn := Socket
|
||||||
|
@ -50,15 +53,30 @@ func DestroyContainer(id string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetContainerIP returns a
|
||||||
func GetContainerIP(id string) (string, error) {
|
func GetContainerIP(id string) (string, error) {
|
||||||
conn := Socket
|
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)
|
container, err := containers.Inspect(conn, id, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Could not get IP of container", err)
|
log.Println("Could not get IP of container", err)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
ip := container.NetworkSettings.IPAddress
|
ip = container.NetworkSettings.IPAddress
|
||||||
|
log.Println(ip)
|
||||||
|
if len(ip) > 5 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i > 50 {
|
||||||
|
log.Println("timed out waiting for IP")
|
||||||
|
}
|
||||||
|
|
||||||
return ip, err
|
return ip, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
package pods
|
package pods
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"time"
|
"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 {
|
func GarbageCollector() error {
|
||||||
|
if viper.GetBool("timeout_on_restart") {
|
||||||
|
timeoutExistingContainers()
|
||||||
|
}
|
||||||
for {
|
for {
|
||||||
err := Cleanup()
|
err := Cleanup()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -13,3 +21,28 @@ func GarbageCollector() error {
|
||||||
time.Sleep(time.Minute * 10)
|
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,7 +3,6 @@ package pods
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containers/podman/v4/pkg/bindings"
|
"github.com/containers/podman/v4/pkg/bindings"
|
||||||
|
@ -14,7 +13,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var Socket context.Context
|
var Socket context.Context
|
||||||
var rawSocket net.Conn
|
|
||||||
|
var OldContainers []string
|
||||||
|
|
||||||
func socketConnection() context.Context {
|
func socketConnection() context.Context {
|
||||||
uri := "unix:///run/podman/podman.sock"
|
uri := "unix:///run/podman/podman.sock"
|
||||||
|
@ -25,22 +25,12 @@ func socketConnection() context.Context {
|
||||||
return conn
|
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() {
|
func ConnectSocket() {
|
||||||
Socket = socketConnection()
|
Socket = socketConnection()
|
||||||
rawSocket = rawConnection()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PullImage() error {
|
func PullImage() error {
|
||||||
|
for {
|
||||||
log.Println("Downloading Container image ", viper.GetString("image"))
|
log.Println("Downloading Container image ", viper.GetString("image"))
|
||||||
image := viper.GetString("image")
|
image := viper.GetString("image")
|
||||||
conn := Socket
|
conn := Socket
|
||||||
|
@ -49,7 +39,10 @@ func PullImage() error {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
time.Sleep(1 * time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup deletes Containers older than the specified maximum Age (Equal to session cookie maximum age)
|
// Cleanup deletes Containers older than the specified maximum Age (Equal to session cookie maximum age)
|
||||||
|
@ -58,11 +51,8 @@ func Cleanup() error {
|
||||||
containerList := containerList()
|
containerList := containerList()
|
||||||
|
|
||||||
for _, container := range containerList {
|
for _, container := range containerList {
|
||||||
now := time.Now()
|
for _, ctid := range OldContainers {
|
||||||
maxAge := time.Second * time.Duration(viper.GetInt("maxAge"))
|
if container.ID == ctid {
|
||||||
containerAge := now.Sub(container.Created)
|
|
||||||
if containerAge > maxAge {
|
|
||||||
|
|
||||||
err := containers.Kill(Socket, container.ID, nil)
|
err := containers.Kill(Socket, container.ID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
@ -75,6 +65,8 @@ func Cleanup() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
32
pods/skel.go
32
pods/skel.go
|
@ -1,10 +1,13 @@
|
||||||
package pods
|
package pods
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/containers/podman/v4/pkg/bindings"
|
"github.com/containers/podman/v4/pkg/bindings"
|
||||||
"github.com/containers/podman/v4/pkg/bindings/containers"
|
"github.com/containers/podman/v4/pkg/bindings/containers"
|
||||||
|
@ -65,7 +68,36 @@ func CopySkelToContainer(id string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Println(response.StatusCode, response.Body)
|
log.Println(response.StatusCode, response.Body)
|
||||||
|
if viper.GetString("skel_user") != "" {
|
||||||
|
chownSkel(id)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Manually chown the Directory, because podmans chown is sometimes unreliable
|
// Manually chown the Directory, because podmans chown is sometimes unreliable
|
||||||
return response.Process(nil)
|
return response.Process(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func chownSkel(id string) {
|
||||||
|
command := fmt.Sprintf(
|
||||||
|
"chown -R %s %s \n\n",
|
||||||
|
viper.GetString("skel_user"),
|
||||||
|
viper.GetString("skel_target"),
|
||||||
|
)
|
||||||
|
done := make(chan bool)
|
||||||
|
detachKeys := "\n\n"
|
||||||
|
commandReader := strings.NewReader(command)
|
||||||
|
var output bytes.Buffer
|
||||||
|
outputWriter := io.Writer(&output)
|
||||||
|
options := &containers.AttachOptions{
|
||||||
|
DetachKeys: &detachKeys,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := containers.Attach(Socket, id, commandReader, outputWriter, outputWriter, done, options)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error: %v \n %s\n", err, output.String())
|
||||||
|
} else {
|
||||||
|
log.Println(output.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -5,22 +5,29 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
"git.jmbit.de/jmb/podterminal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func readConfigFile() {
|
func readConfigFile() {
|
||||||
|
sessionKey, _ := utils.RandomString(64)
|
||||||
log.Println("Reading Config")
|
log.Println("Reading Config")
|
||||||
viper.SetConfigFile("/etc/podterminal/config.yaml")
|
viper.SetConfigFile("/etc/podterminal/config.yaml")
|
||||||
viper.SetDefault("port", 80)
|
viper.SetDefault("port", 80)
|
||||||
viper.SetDefault("ip_addr", "0.0.0.0")
|
viper.SetDefault("ip_addr", "0.0.0.0")
|
||||||
viper.SetDefault("image", "lscr.io/linuxserver/webtop")
|
viper.SetDefault("image", "git.jmbit.de/jmb/webtop-plus:latest")
|
||||||
viper.SetDefault("maxAge", 10800)
|
viper.SetDefault("maxAge", 10800)
|
||||||
viper.SetDefault("dri", false)
|
viper.SetDefault("dri", false)
|
||||||
viper.SetDefault("dir_node", "/dev/dri/renderD128")
|
viper.SetDefault("dir_node", "/dev/dri/renderD128")
|
||||||
viper.SetDefault("skel", "/etc/podterminal/skel.tar.gz")
|
viper.SetDefault("skel", "/etc/podterminal/skel.tar.gz")
|
||||||
viper.SetDefault("skel_chown", false)
|
viper.SetDefault("skel_chown", false)
|
||||||
viper.SetDefault("skel_target", "/")
|
viper.SetDefault("skel_target", "/")
|
||||||
viper.SetDefault("skel_owner", "abc")
|
viper.SetDefault("skel_user", "")
|
||||||
viper.SetDefault("block_filebrowser", false)
|
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",
|
viper.SetDefault("envvars",
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"CUSTOM_USER": "user",
|
"CUSTOM_USER": "user",
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package web
|
||||||
|
|
||||||
|
var clientReloadContent = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title> Reloading </title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
reloading
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
setTimeout(() => {
|
||||||
|
document.location.reload();
|
||||||
|
}, 3000);
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
`
|
|
@ -3,12 +3,14 @@ package web
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"git.jmbit.de/jmb/podterminal/pods"
|
"git.jmbit.de/jmb/podterminal/pods"
|
||||||
)
|
)
|
||||||
|
@ -27,6 +29,15 @@ func createReverseProxy(backendService string) (*httputil.ReverseProxy, error) {
|
||||||
request.SetURL(backendURL)
|
request.SetURL(backendURL)
|
||||||
request.Out.Host = request.In.Host
|
request.Out.Host = request.In.Host
|
||||||
},
|
},
|
||||||
|
// ModifyResponse: func(response *http.Response) error {
|
||||||
|
// if response.StatusCode == http.StatusBadGateway {
|
||||||
|
// waitForAnswer(backendURL.String())
|
||||||
|
// response.StatusCode = 200
|
||||||
|
// response.Header.Set("Location", "/")
|
||||||
|
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
|
// },
|
||||||
}
|
}
|
||||||
|
|
||||||
return proxy, err
|
return proxy, err
|
||||||
|
@ -34,33 +45,48 @@ func createReverseProxy(backendService string) (*httputil.ReverseProxy, error) {
|
||||||
|
|
||||||
func containerProxy(c *gin.Context) {
|
func containerProxy(c *gin.Context) {
|
||||||
session := sessions.Default(c)
|
session := sessions.Default(c)
|
||||||
session.Save()
|
sessionID := ""
|
||||||
sessionID := session.ID()
|
|
||||||
if session.Get("ct") == nil {
|
if session.Get("ct") == nil {
|
||||||
|
session.Set("ready", false)
|
||||||
|
session.Save()
|
||||||
log.Println("Creating Container for Session ", sessionID)
|
log.Println("Creating Container for Session ", sessionID)
|
||||||
ct, err := pods.CreateContainer()
|
ct, err := pods.CreateContainer()
|
||||||
|
session.Set("ct", ct)
|
||||||
|
session.Save()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.HTML(500, "Error", fmt.Sprintf("[%s] Could not create Container: %v", sessionID, err))
|
c.HTML(500, "Error", fmt.Sprintf("[%s] Could not create Container: %v", sessionID, err))
|
||||||
|
session.Delete("ct")
|
||||||
|
session.Save()
|
||||||
c.Abort()
|
c.Abort()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
err = pods.StartContainer(ct)
|
err = pods.StartContainer(ct)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.HTML(500, "Error", fmt.Sprintf("[%s] Could not start Container: %v", sessionID, err))
|
c.HTML(500, "Error", fmt.Sprintf("[%s] Could not start Container: %v", sessionID, err))
|
||||||
|
session.Delete("ct")
|
||||||
|
session.Save()
|
||||||
c.Abort()
|
c.Abort()
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
// Hack to wait for Container to start up and get assigned an IP
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
ctip, err := pods.GetContainerIP(ct)
|
ctip, err := pods.GetContainerIP(ct)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.HTML(500, "Error", fmt.Sprintf("[%s] Could not get Container ip: %v", sessionID, err))
|
c.HTML(500, "Error", fmt.Sprintf("[%s] Could not get Container ip: %v", sessionID, err))
|
||||||
|
session.Delete("ct")
|
||||||
|
session.Save()
|
||||||
c.Abort()
|
c.Abort()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Soft fail Skel
|
// Soft fail Skel
|
||||||
_ = pods.CopySkelToContainer(ct)
|
// _ = pods.CopySkelToContainer(ct)
|
||||||
|
|
||||||
proxies[ct], err = createReverseProxy(fmt.Sprintf("http://%s:3000", ctip))
|
err = waitForAnswer(fmt.Sprintf("http://%s:%d", ctip, viper.GetInt("container_port")))
|
||||||
|
proxies[ct], err = createReverseProxy(
|
||||||
|
fmt.Sprintf("http://%s:%d", ctip, viper.GetInt("container_port")),
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.HTML(
|
c.HTML(
|
||||||
|
@ -68,23 +94,65 @@ func containerProxy(c *gin.Context) {
|
||||||
"Error",
|
"Error",
|
||||||
fmt.Sprintf("[%s] Could not create Container Proxy: %v", sessionID, err),
|
fmt.Sprintf("[%s] Could not create Container Proxy: %v", sessionID, err),
|
||||||
)
|
)
|
||||||
c.Abort()
|
session.Delete("ct")
|
||||||
}
|
|
||||||
session.Set("ct", ct)
|
|
||||||
session.Save()
|
session.Save()
|
||||||
c.Redirect(301, "/")
|
|
||||||
} else {
|
|
||||||
sessionCT := session.Get("ct")
|
|
||||||
switch sessionCT.(type) {
|
|
||||||
case string:
|
|
||||||
|
|
||||||
default:
|
|
||||||
c.HTML(500, "Error", "Session Container ID is not a string")
|
|
||||||
c.Abort()
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(
|
||||||
|
500,
|
||||||
|
"Error",
|
||||||
|
fmt.Sprintf("[%s] Timed out waiting for Container: %v", sessionID, err),
|
||||||
|
)
|
||||||
|
session.Delete("ct")
|
||||||
|
session.Save()
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session.Set("ready", true)
|
||||||
|
session.Save()
|
||||||
|
c.HTML(200, "", clientReloadContent)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
if session.Get("ready").(bool) == false {
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
c.Redirect(307, "/")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
id := session.Get("ct").(string)
|
id := session.Get("ct").(string)
|
||||||
proxy := proxies[id]
|
proxy := proxies[id]
|
||||||
|
if proxy != nil {
|
||||||
proxy.ServeHTTP(c.Writer, c.Request)
|
proxy.ServeHTTP(c.Writer, c.Request)
|
||||||
|
} else {
|
||||||
|
session.Delete("ct")
|
||||||
|
session.Delete("ready")
|
||||||
|
session.Save()
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func waitForAnswer(url string) error {
|
||||||
|
retries := 0
|
||||||
|
var err error
|
||||||
|
for retries < 50 {
|
||||||
|
err = nil
|
||||||
|
response, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error connecting to %s: %v", url, err)
|
||||||
|
}
|
||||||
|
if response.StatusCode == 200 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
retries++
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
log.Println("Timed out waiting for Container")
|
||||||
|
return err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-contrib/sessions/cookie"
|
"github.com/gin-contrib/sessions/cookie"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,7 +41,7 @@ func setupRouter() *gin.Engine {
|
||||||
gin.ForceConsoleColor()
|
gin.ForceConsoleColor()
|
||||||
gin.SetMode("release")
|
gin.SetMode("release")
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
store := cookie.NewStore([]byte(uuid.NewString()))
|
store := cookie.NewStore([]byte(viper.GetString("session_key")))
|
||||||
store.Options(sessions.Options{
|
store.Options(sessions.Options{
|
||||||
MaxAge: viper.GetInt("maxAge"),
|
MaxAge: viper.GetInt("maxAge"),
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
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