From 241abdfca4e350d543b502a0c442d934f0b86d5a Mon Sep 17 00:00:00 2001 From: Yann Stepienik Date: Fri, 5 May 2023 19:05:33 +0100 Subject: [PATCH] [release] v0.4.0-unstable2 --- changelog.md | 2 + client/src/api/docker.demo.jsx | 11 +- client/src/api/docker.jsx | 12 ++- client/src/pages/servapps/servapps.jsx | 141 ++++++++++++++++++++----- package.json | 2 +- src/docker/api_managecont.go | 94 +++++++++++++++++ src/docker/docker.go | 18 ---- src/httpServer.go | 14 ++- src/index.go | 3 - src/utils/types.go | 1 + 10 files changed, 247 insertions(+), 51 deletions(-) create mode 100644 src/docker/api_managecont.go diff --git a/changelog.md b/changelog.md index 26965e6..8c6fd95 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,7 @@ ## Version 0.4.0 - Protect server against direct IP access + - Stop / Start / Restart / Remove / Kill containers + - ## Version 0.3.0 - Implement 2 FA diff --git a/client/src/api/docker.demo.jsx b/client/src/api/docker.demo.jsx index 1c67705..6316d5b 100644 --- a/client/src/api/docker.demo.jsx +++ b/client/src/api/docker.demo.jsx @@ -22,8 +22,17 @@ const newDB = () => { }); } +const manageContainer = () => { + return new Promise((resolve, reject) => { + resolve({ + "status": "ok", + }) + }); +} + export { list, newDB, - secure + secure, + manageContainer }; \ No newline at end of file diff --git a/client/src/api/docker.jsx b/client/src/api/docker.jsx index 4df500f..563e755 100644 --- a/client/src/api/docker.jsx +++ b/client/src/api/docker.jsx @@ -26,9 +26,19 @@ const newDB = () => { } })) } + +const manageContainer = (id, action) => { + return wrap(fetch('/cosmos/api/servapps/' + id + '/manage/' + action, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + })) +} export { list, newDB, - secure + secure, + manageContainer }; \ No newline at end of file diff --git a/client/src/pages/servapps/servapps.jsx b/client/src/pages/servapps/servapps.jsx index fd2fe28..57c0d87 100644 --- a/client/src/pages/servapps/servapps.jsx +++ b/client/src/pages/servapps/servapps.jsx @@ -1,6 +1,6 @@ // material-ui -import { AppstoreAddOutlined, PlusCircleOutlined, ReloadOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons'; -import { Alert, Badge, Button, Card, Checkbox, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Input, InputAdornment, TextField, Tooltip, Typography } from '@mui/material'; +import { AppstoreAddOutlined, CloseSquareOutlined, DeleteOutlined, PauseCircleOutlined, PlaySquareOutlined, PlusCircleOutlined, ReloadOutlined, RollbackOutlined, SearchOutlined, SettingOutlined, StopOutlined, UpCircleOutlined, UpSquareFilled } from '@ant-design/icons'; +import { Alert, Badge, Button, Card, Checkbox, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, IconButton, Input, InputAdornment, TextField, Tooltip, Typography } from '@mui/material'; import Grid2 from '@mui/material/Unstable_Grid2/Grid2'; import { Stack } from '@mui/system'; import { useEffect, useState } from 'react'; @@ -126,6 +126,88 @@ const ServeApps = () => { } } + const getActions = (app) => { + const doTo = (action) => { + setIsUpdatingId(app.Id, true); + API.docker.manageContainer(app.Id, action).then((res) => { + refreshServeApps(); + }); + }; + + let actions = [ + { + t: 'Update Available', + if: ['update_available'], + e: {doTo('update')}} size='large'> + + + }, + { + t: 'Start', + if: ['exited'], + e: {doTo('start')}} size='large'> + + + }, + { + t: 'Unpause', + if: ['paused'], + e: {doTo('unpause')}} size='large'> + + + }, + { + t: 'Pause', + if: ['running'], + e: {doTo('pause')}} size='large'> + + + }, + { + t: 'Stop', + if: ['created', 'paused', 'restarting', 'running'], + e: {doTo('stop')}} size='large' variant="outlined"> + + + }, + { + t: 'Restart', + if: ['exited', 'running', 'paused', 'created', 'restarting'], + e: doTo('restart')} size='large'> + + + }, + { + t: 'Re-create', + if: ['exited', 'running', 'paused', 'created', 'restarting'], + e: doTo('recreate')} color="error" size='large'> + + + }, + { + t: 'Delete', + if: ['exited'], + e: {doTo('remove')}} color="error" size='large'> + + + }, + { + t: 'Kill', + if: ['running', 'paused', 'created', 'restarting'], + e: doTo('kill')} color="error" size='large'> + + + } + ]; + + return actions.filter((action) => { + let updateAvailable = false; + return action.if.includes(app.State) ?? (updateAvailable && action.if.includes('update_available')); + }).map((action) => { + return {action.e} + }); + } + return
@@ -229,31 +311,40 @@ const ServeApps = () => { return }> - - - { - ({ - "created": , - "restarting": , - "running": , - "removing": , - "paused": , - "exited": , - "dead": , - })[app.State] - } - - - - - - {app.Names[0].replace('/', '')}  - - - {app.Image} - + + + + { + ({ + "created": , + "restarting": , + "running": , + "removing": , + "paused": , + "exited": , + "dead": , + })[app.State] + } + + + + + + {app.Names[0].replace('/', '')}  + + + {app.Image} + + + + {/* */} + + {getActions(app)} + diff --git a/package.json b/package.json index e5d974c..4e99b4e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cosmos-server", - "version": "0.4.0-unstable", + "version": "0.4.0-unstable2", "description": "", "main": "test-server.js", "bugs": { diff --git a/src/docker/api_managecont.go b/src/docker/api_managecont.go new file mode 100644 index 0000000..35cdd28 --- /dev/null +++ b/src/docker/api_managecont.go @@ -0,0 +1,94 @@ +package docker + +import ( + "net/http" + "encoding/json" + "io" + "os" + + "github.com/azukaar/cosmos-server/src/utils" + + "github.com/gorilla/mux" + // "github.com/docker/docker/client" + contstuff "github.com/docker/docker/api/types/container" + doctype "github.com/docker/docker/api/types" +) + +func ManageContainerRoute(w http.ResponseWriter, req *http.Request) { + if utils.AdminOnly(w, req) != nil { + return + } + + errD := Connect() + if errD != nil { + utils.Error("ManageContainer", errD) + utils.HTTPError(w, "Internal server error: " + errD.Error(), http.StatusInternalServerError, "DS002") + return + } + + vars := mux.Vars(req) + containerName := utils.Sanitize(vars["containerId"]) + // stop, start, restart, kill, remove, pause, unpause, recreate + action := utils.Sanitize(vars["action"]) + + utils.Log("ManageContainer " + containerName) + + if req.Method == "GET" { + container, err := DockerClient.ContainerInspect(DockerContext, containerName) + if err != nil { + utils.Error("ManageContainer", err) + utils.HTTPError(w, "Internal server error: " + err.Error(), http.StatusInternalServerError, "DS002") + return + } + + imagename := container.Config.Image + + utils.Log("API: " + action + " " + containerName) + + // switch action + switch action { + case "stop": + err = DockerClient.ContainerStop(DockerContext, container.ID, contstuff.StopOptions{}) + case "start": + err = DockerClient.ContainerStart(DockerContext, container.ID, doctype.ContainerStartOptions{}) + case "restart": + err = DockerClient.ContainerRestart(DockerContext, container.ID, contstuff.StopOptions{}) + case "kill": + err = DockerClient.ContainerKill(DockerContext, container.ID, "") + case "remove": + err = DockerClient.ContainerRemove(DockerContext, container.ID, doctype.ContainerRemoveOptions{}) + case "pause": + err = DockerClient.ContainerPause(DockerContext, container.ID) + case "unpause": + err = DockerClient.ContainerUnpause(DockerContext, container.ID) + case "recreate": + _, err = EditContainer(container.ID, container) + case "update": + pull, errPull := DockerClient.ImagePull(DockerContext, imagename, doctype.ImagePullOptions{}) + if errPull != nil { + utils.Error("Docker Pull", errPull) + utils.HTTPError(w, "Cannot pull new image", http.StatusBadRequest, "DS004") + return + } + io.Copy(os.Stdout, pull) + _, err = EditContainer(container.ID, container) + default: + utils.HTTPError(w, "Invalid action", http.StatusBadRequest, "DS003") + return + } + + if err != nil { + utils.Error("ManageContainer: " + action, err) + utils.HTTPError(w, "Internal server error: " + err.Error(), http.StatusInternalServerError, "DS004") + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + }) + } else { + utils.Error("ManageContainer: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} \ No newline at end of file diff --git a/src/docker/docker.go b/src/docker/docker.go index 739b708..3be69dd 100644 --- a/src/docker/docker.go +++ b/src/docker/docker.go @@ -190,24 +190,6 @@ func ListContainers() ([]types.Container, error) { return nil, err } - // for _, container := range containers { - // fmt.Println("ID - ", container.ID) - // fmt.Println("ID - ", container.Names) - // fmt.Println("ID - ", container.Image) - // fmt.Println("ID - ", container.Command) - // fmt.Println("ID - ", container.State) - // fmt.Println("Ports - ", container.Ports) - // fmt.Println("HostConfig - ", container.HostConfig) - // fmt.Println("ID - ", container.Labels) - // fmt.Println("NetworkSettings - ", container.NetworkSettings) - // if(container.NetworkSettings.Networks["cosmos-network"] != nil) { - // fmt.Println("IP COSMOS - ", container.NetworkSettings.Networks["cosmos-network"].IPAddress); - // } - // if(container.NetworkSettings.Networks["bridge"] != nil) { - // fmt.Println("IP bridge - ", container.NetworkSettings.Networks["bridge"].IPAddress); - // } - // } - return containers, nil } diff --git a/src/httpServer.go b/src/httpServer.go index 10c5a16..09acf7b 100644 --- a/src/httpServer.go +++ b/src/httpServer.go @@ -220,10 +220,14 @@ func StartServer() { srapi.HandleFunc("/api/users/{nickname}", user.UsersIdRoute) srapi.HandleFunc("/api/users", user.UsersRoute) + srapi.HandleFunc("/api/servapps/{containerId}/manage/{action}", docker.ManageContainerRoute) srapi.HandleFunc("/api/servapps/{containerId}/secure/{status}", docker.SecureContainerRoute) srapi.HandleFunc("/api/servapps", docker.ContainersRoute) - srapi.Use(utils.EnsureHostname(serverHostname)) + // if(!config.HTTPConfig.AcceptAllInsecureHostname) { + // srapi.Use(utils.EnsureHostname(serverHostname)) + // } + srapi.Use(tokenMiddleware) srapi.Use(proxy.SmartShieldMiddleware( utils.SmartShieldPolicy{ @@ -251,8 +255,14 @@ func StartServer() { utils.Fatal("Static folder not found at " + pwd + "/static", err) } + fs := spa.SpaHandler(pwd + "/static", "index.html") - router.PathPrefix("/ui").Handler(utils.EnsureHostname(serverHostname)(http.StripPrefix("/ui", fs))) + + // if(!config.HTTPConfig.AcceptAllInsecureHostname) { + // fs = utils.EnsureHostname(serverHostname)(fs) + // } + + router.PathPrefix("/ui").Handler(http.StripPrefix("/ui", fs)) router = proxy.BuildFromConfig(router, HTTPConfig.ProxyConfig) diff --git a/src/index.go b/src/index.go index de1ac3b..4accfb7 100644 --- a/src/index.go +++ b/src/index.go @@ -10,14 +10,11 @@ import ( func main() { utils.Log("Starting...") - // utils.Log("Smart Shield estimates the capacity at " + strconv.Itoa((int)(proxy.MaxUsers)) + " concurrent users") rand.Seed(time.Now().UnixNano()) LoadConfig() - checkVersion() - go CRON() docker.Test() diff --git a/src/utils/types.go b/src/utils/types.go index 7476bc6..628f429 100644 --- a/src/utils/types.go +++ b/src/utils/types.go @@ -101,6 +101,7 @@ type HTTPConfig struct { ProxyConfig ProxyConfig Hostname string `validate:"required,excludesall=0x2C/ "` SSLEmail string `validate:"omitempty,email"` + AcceptAllInsecureHostname bool } const (