From 467f84187f54319a83ef196e1ca53fd03661e4aa Mon Sep 17 00:00:00 2001 From: Yann Stepienik Date: Tue, 16 May 2023 18:08:01 +0100 Subject: [PATCH] [release] version 0.5.0-unstable11 --- changelog.md | 1 + client/src/api/docker.jsx | 10 ++++ client/src/pages/servapps/actionBar.jsx | 48 +++++++++++++--- .../pages/servapps/containers/overview.jsx | 15 +++++ client/src/pages/servapps/servapps.jsx | 29 ++++++++-- package.json | 2 +- src/docker/api_autoupdate.go | 57 +++++++++++++++++++ src/docker/api_blueprint.go | 4 +- src/httpServer.go | 1 + src/index.go | 4 ++ src/user/2fa_check.go | 2 +- src/user/login.go | 2 +- src/user/logout.go | 2 +- src/user/token.go | 56 ++++++++++++++---- src/utils/middleware.go | 38 +++++++++++++ 15 files changed, 241 insertions(+), 30 deletions(-) create mode 100644 src/docker/api_autoupdate.go diff --git a/changelog.md b/changelog.md index aa19178..80282af 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,7 @@ - Add Create Container - Add support for importing Docker Compose - Fixed 2 bugs with the smart shield, that made it too strict + - Fixed issues that prevented from login in with different hostnames - Added more infoon the shield when blocking someone - Fixed home background image diff --git a/client/src/api/docker.jsx b/client/src/api/docker.jsx index 73adb85..7fbfbe8 100644 --- a/client/src/api/docker.jsx +++ b/client/src/api/docker.jsx @@ -268,6 +268,15 @@ function pullImage(imageName, onProgress, ifMissing) { }); } +function autoUpdate(id, toggle) { + return wrap(fetch('/cosmos/api/servapps/' + id + '/auto-update/'+toggle, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + }, + })) +} + export { list, get, @@ -289,4 +298,5 @@ export { createTerminal, createService, pullImage, + autoUpdate, }; \ No newline at end of file diff --git a/client/src/pages/servapps/actionBar.jsx b/client/src/pages/servapps/actionBar.jsx index 2ed657d..14e01a9 100644 --- a/client/src/pages/servapps/actionBar.jsx +++ b/client/src/pages/servapps/actionBar.jsx @@ -1,24 +1,39 @@ import React from 'react'; -import { IconButton, Tooltip, useMediaQuery } from '@mui/material'; +import { Box, IconButton, LinearProgress, Stack, Tooltip, useMediaQuery } from '@mui/material'; import { CheckCircleOutlined, CloseSquareOutlined, DeleteOutlined, PauseCircleOutlined, PlaySquareOutlined, ReloadOutlined, RollbackOutlined, StopOutlined, UpCircleOutlined } from '@ant-design/icons'; import * as API from '../../api'; +import LogsInModal from '../../components/logsInModal'; const GetActions = ({ Id, state, + image, refreshServeApps, setIsUpdatingId, updateAvailable }) => { const [confirmDelete, setConfirmDelete] = React.useState(false); const isMiniMobile = useMediaQuery((theme) => theme.breakpoints.down('xsm')); + const [pullRequest, setPullRequest] = React.useState(null); + const [isUpdating, setIsUpdating] = React.useState(false); console.log(isMiniMobile) const doTo = (action) => { + setIsUpdating(true); + + if(action === 'pull') { + setPullRequest(() => ((cb) => { + API.docker.pullImage(image, cb, true) + })); + return; + } + setIsUpdatingId(Id, true); - API.docker.manageContainer(Id, action).then((res) => { + return API.docker.manageContainer(Id, action).then((res) => { + setIsUpdating(false); refreshServeApps(); }).catch((err) => { + setIsUpdating(false); refreshServeApps(); }); }; @@ -27,7 +42,7 @@ const GetActions = ({ { t: 'Update Available', if: ['update_available'], - e: {doTo('update')}} size={isMiniMobile ? 'medium' : 'large'}> + e: {doTo('pull')}} size={isMiniMobile ? 'medium' : 'large'}> }, @@ -93,11 +108,28 @@ const GetActions = ({ } ]; - return actions.filter((action) => { - return action.if.includes(state) || (updateAvailable && action.if.includes('update_available')); - }).map((action) => { - return {action.e} - }); + return <> + { + doTo('update') + }} + /> + + {!isUpdating && actions.filter((action) => { + updateAvailable = true; + return action.if.includes(state) || (updateAvailable && action.if.includes('update_available')); + }).map((action) => { + return {action.e} + })} + + {isUpdating && } + } export default GetActions; \ No newline at end of file diff --git a/client/src/pages/servapps/containers/overview.jsx b/client/src/pages/servapps/containers/overview.jsx index 338190f..230a435 100644 --- a/client/src/pages/servapps/containers/overview.jsx +++ b/client/src/pages/servapps/containers/overview.jsx @@ -91,6 +91,7 @@ const ContainerOverview = ({ containerInfo, config, refresh }) => { { refreshAll() @@ -130,6 +131,20 @@ const ContainerOverview = ({ containerInfo, config, refresh }) => { }} /> Force Secure Network + + { + setIsUpdating(true); + API.docker.autoUpdate(Name, e.target.checked).then(() => { + setTimeout(() => { + refreshAll(); + }, 3000); + }) + }} + /> Auto Update Container + URLs
{routes.map((route) => { diff --git a/client/src/pages/servapps/servapps.jsx b/client/src/pages/servapps/servapps.jsx index 7e6fab1..ec419f9 100644 --- a/client/src/pages/servapps/servapps.jsx +++ b/client/src/pages/servapps/servapps.jsx @@ -188,16 +188,14 @@ const ServeApps = () => { - - {/* */} + @@ -242,10 +240,31 @@ const ServeApps = () => { setIsUpdatingId(app.Id, false); refreshServeApps(); }, 3000); + }).catch(() => { + setIsUpdatingId(app.Id, false); + refreshServeApps(); }) }} /> Force Secure Network + + { + setIsUpdatingId(app.Id, true); + API.docker.autoUpdate(app.Id, e.target.checked).then(() => { + setTimeout(() => { + setIsUpdatingId(app.Id, false); + refreshServeApps(); + }, 3000); + }).catch(() => { + setIsUpdatingId(app.Id, false); + refreshServeApps(); + }) + }} + /> Auto Update Container + } diff --git a/package.json b/package.json index 090712a..0116444 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cosmos-server", - "version": "0.5.0-unstable10", + "version": "0.5.0-unstable11", "description": "", "main": "test-server.js", "bugs": { diff --git a/src/docker/api_autoupdate.go b/src/docker/api_autoupdate.go new file mode 100644 index 0000000..5683275 --- /dev/null +++ b/src/docker/api_autoupdate.go @@ -0,0 +1,57 @@ +package docker + +import ( + "net/http" + "encoding/json" + "os" + + "github.com/azukaar/cosmos-server/src/utils" + + "github.com/gorilla/mux" +) + +func AutoUpdateContainerRoute(w http.ResponseWriter, req *http.Request) { + if utils.AdminOnly(w, req) != nil { + return + } + + vars := mux.Vars(req) + containerName := utils.SanitizeSafe(vars["containerId"]) + status := utils.Sanitize(vars["status"]) + + if os.Getenv("HOSTNAME") != "" && containerName == os.Getenv("HOSTNAME") { + utils.Error("AutoUpdateContainerRoute - Container cannot update itself", nil) + utils.HTTPError(w, "Container cannot update itself", http.StatusBadRequest, "DS003") + return + } + + if(req.Method == "GET") { + container, err := DockerClient.ContainerInspect(DockerContext, containerName) + if err != nil { + utils.Error("AutoUpdateContainer Inscpect", err) + utils.HTTPError(w, "Internal server error: " + err.Error(), http.StatusInternalServerError, "DS002") + return + } + + AddLabels(container, map[string]string{ + "cosmos-auto-update": status, + }); + + utils.Log("API: Set Auto Update "+status+" : " + containerName) + + _, errEdit := EditContainer(container.ID, container) + if errEdit != nil { + utils.Error("AutoUpdateContainer Edit", errEdit) + utils.HTTPError(w, "Internal server error: " + errEdit.Error(), http.StatusInternalServerError, "DS003") + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + }) + } else { + utils.Error("AutoUpdateContainer: 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/api_blueprint.go b/src/docker/api_blueprint.go index 4ebb95f..f0c8e53 100644 --- a/src/docker/api_blueprint.go +++ b/src/docker/api_blueprint.go @@ -161,7 +161,7 @@ func CreateServiceRoute(w http.ResponseWriter, req *http.Request) { errD := Connect() if errD != nil { - utils.Error("CreateService", errD) + utils.Error("CreateService - connect - ", errD) utils.HTTPError(w, "Internal server error: " + errD.Error(), http.StatusInternalServerError, "DS002") return } @@ -191,7 +191,7 @@ func CreateServiceRoute(w http.ResponseWriter, req *http.Request) { var serviceRequest DockerServiceCreateRequest err := decoder.Decode(&serviceRequest) if err != nil { - utils.Error("CreateService", err) + utils.Error("CreateService - decode - ", err) utils.HTTPError(w, "Bad request: "+err.Error(), http.StatusBadRequest, "DS003") return } diff --git a/src/httpServer.go b/src/httpServer.go index 6e31b5f..565d9bb 100644 --- a/src/httpServer.go +++ b/src/httpServer.go @@ -230,6 +230,7 @@ func StartServer() { srapi.HandleFunc("/api/servapps/{containerId}/manage/{action}", docker.ManageContainerRoute) srapi.HandleFunc("/api/servapps/{containerId}/secure/{status}", docker.SecureContainerRoute) + srapi.HandleFunc("/api/servapps/{containerId}/auto-update/{status}", docker.AutoUpdateContainerRoute) srapi.HandleFunc("/api/servapps/{containerId}/logs", docker.GetContainerLogsRoute) srapi.HandleFunc("/api/servapps/{containerId}/terminal/{action}", docker.TerminalRoute) srapi.HandleFunc("/api/servapps/{containerId}/update", docker.UpdateContainerRoute) diff --git a/src/index.go b/src/index.go index 758a212..54671b8 100644 --- a/src/index.go +++ b/src/index.go @@ -24,6 +24,10 @@ func main() { docker.BootstrapAllContainersFromTags() + // TODO DELET THIS BEFORE RELEASE + + docker.CheckUpdatesAvailable() + version, err := docker.DockerClient.ServerVersion(context.Background()) if err == nil { utils.Log("Docker API version: " + version.APIVersion) diff --git a/src/user/2fa_check.go b/src/user/2fa_check.go index de303d2..2a1513f 100644 --- a/src/user/2fa_check.go +++ b/src/user/2fa_check.go @@ -75,7 +75,7 @@ func Check2FA(w http.ResponseWriter, req *http.Request) { } } - SendUserToken(w, userInBase, true) + SendUserToken(w, req, userInBase, true) json.NewEncoder(w).Encode(map[string]interface{}{ "status": "OK", diff --git a/src/user/login.go b/src/user/login.go index fa230aa..1c6e701 100644 --- a/src/user/login.go +++ b/src/user/login.go @@ -70,7 +70,7 @@ func UserLogin(w http.ResponseWriter, req *http.Request) { return } - SendUserToken(w, user, false) + SendUserToken(w, req, user, false) json.NewEncoder(w).Encode(map[string]interface{}{ "status": "OK", diff --git a/src/user/logout.go b/src/user/logout.go index a6e5ad0..d31301f 100644 --- a/src/user/logout.go +++ b/src/user/logout.go @@ -10,7 +10,7 @@ func UserLogout(w http.ResponseWriter, req *http.Request) { if(req.Method == "GET") { utils.Debug("UserLogout: Logging out user") - logOutUser(w); + logOutUser(w, req); json.NewEncoder(w).Encode(map[string]interface{}{ "status": "OK", diff --git a/src/user/token.go b/src/user/token.go index bbb6a27..d2fa71e 100644 --- a/src/user/token.go +++ b/src/user/token.go @@ -12,7 +12,7 @@ import ( func quickLoggout(w http.ResponseWriter, req *http.Request, err error) (utils.User, error) { utils.Error("UserToken: Token likely falsified", err) - logOutUser(w) + logOutUser(w, req) redirectToReLogin(w, req) return utils.User{}, errors.New("Token likely falsified") } @@ -75,7 +75,7 @@ func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, err if errP != nil { utils.Error("UserToken: token is not valid", nil) - logOutUser(w) + logOutUser(w, req) redirectToReLogin(w, req) return utils.User{}, errors.New("Token not valid") } @@ -85,6 +85,7 @@ func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, err passwordCycle int mfaDone bool ok bool + forDomain string ) if nickname, ok = claims["nickname"].(string); !ok { @@ -107,6 +108,22 @@ func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, err } } + if forDomain, ok = claims["forDomain"].(string); !ok { + if _, e := quickLoggout(w, req, nil); e != nil { + return utils.User{}, e + } + } + + reqHostname := req.Host + reqHostNoPort := strings.Split(reqHostname, ":")[0] + + if !strings.HasSuffix(reqHostNoPort, forDomain) { + utils.Error("UserToken: token is not valid for this domain", nil) + logOutUser(w, req) + redirectToReLogin(w, req) + return utils.User{}, errors.New("JWT Token not valid for this domain") + } + userInBase := utils.User{} c, errCo := utils.GetCollection(utils.GetRootAppId(), "users") @@ -122,14 +139,14 @@ func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, err if errDB != nil { utils.Error("UserToken: User not found", errDB) - logOutUser(w) + logOutUser(w, req) redirectToReLogin(w, req) return utils.User{}, errors.New("User not found") } if userInBase.PasswordCycle != passwordCycle { utils.Error("UserToken: Password cycle changed, token is too old", nil) - logOutUser(w) + logOutUser(w, req) redirectToReLogin(w, req) return utils.User{}, errors.New("Password cycle changed, token is too old") } @@ -148,7 +165,7 @@ func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, err } if time.Now().Unix() - int64(claims["iat"].(float64)) > 3600 { - SendUserToken(w, userInBase, mfaDone) + SendUserToken(w, req, userInBase, mfaDone) } return userInBase, nil @@ -159,7 +176,10 @@ func GetUserR(req *http.Request) (string, string) { } -func logOutUser(w http.ResponseWriter) { +func logOutUser(w http.ResponseWriter, req *http.Request) { + reqHostname := req.Host + reqHostNoPort := strings.Split(reqHostname, ":")[0] + cookie := http.Cookie{ Name: "jwttoken", Value: "", @@ -167,11 +187,12 @@ func logOutUser(w http.ResponseWriter) { Path: "/", Secure: utils.IsHTTPS, HttpOnly: true, - Domain: utils.GetMainConfig().HTTPConfig.Hostname, } - if(utils.GetMainConfig().HTTPConfig.Hostname == "localhost" || utils.GetMainConfig().HTTPConfig.Hostname == "0.0.0.0") { + if reqHostNoPort == "localhost" || reqHostNoPort == "0.0.0.0" { cookie.Domain = "" + } else { + cookie.Domain = "." + reqHostNoPort } http.SetCookie(w, &cookie) @@ -191,7 +212,10 @@ func redirectToNewMFA(w http.ResponseWriter, req *http.Request) { http.Redirect(w, req, "/ui/newmfa?invalid=1&redirect=" + req.URL.Path + "&" + req.URL.RawQuery, http.StatusTemporaryRedirect) } -func SendUserToken(w http.ResponseWriter, user utils.User, mfaDone bool) { +func SendUserToken(w http.ResponseWriter, req *http.Request, user utils.User, mfaDone bool) { + reqHostname := req.Host + reqHostNoPort := strings.Split(reqHostname, ":")[0] + expiration := time.Now().Add(3 * 24 * time.Hour) token := jwt.New(jwt.SigningMethodEdDSA) @@ -203,6 +227,7 @@ func SendUserToken(w http.ResponseWriter, user utils.User, mfaDone bool) { claims["iat"] = time.Now().Unix() claims["nbf"] = time.Now().Unix() claims["mfaDone"] = mfaDone + claims["forDomain"] = reqHostNoPort key, err5 := jwt.ParseEdPrivateKeyFromPEM([]byte(utils.GetPrivateAuthKey())) @@ -227,11 +252,20 @@ func SendUserToken(w http.ResponseWriter, user utils.User, mfaDone bool) { Path: "/", Secure: utils.IsHTTPS, HttpOnly: true, - Domain: utils.GetMainConfig().HTTPConfig.Hostname, } - if(utils.GetMainConfig().HTTPConfig.Hostname == "localhost" || utils.GetMainConfig().HTTPConfig.Hostname == "0.0.0.0") { + utils.Log("UserLogin: Setting cookie for " + reqHostNoPort) + + if reqHostNoPort == "localhost" || reqHostNoPort == "0.0.0.0" { cookie.Domain = "" + } else { + if utils.IsValidHostname(reqHostNoPort) { + cookie.Domain = "." + reqHostNoPort + } else { + utils.Error("UserLogin: Invalid hostname", nil) + utils.HTTPError(w, "User Logging Error", http.StatusInternalServerError, "UL001") + return + } } http.SetCookie(w, &cookie) diff --git a/src/utils/middleware.go b/src/utils/middleware.go index ab88e3f..9a821cc 100644 --- a/src/utils/middleware.go +++ b/src/utils/middleware.go @@ -202,4 +202,42 @@ func EnsureHostname(next http.Handler) http.Handler { next.ServeHTTP(w, r) }) +} + +func IsValidHostname(hostname string) bool { + og := GetMainConfig().HTTPConfig.Hostname + ni := GetMainConfig().NewInstall + + if ni || og == "0.0.0.0" { + return true + } + + hostnames := GetAllHostnames() + + reqHostNoPort := strings.Split(hostname, ":")[0] + + reqHostNoPortNoSubdomain := "" + + if parts := strings.Split(reqHostNoPort, "."); len(parts) < 2 { + reqHostNoPortNoSubdomain = reqHostNoPort + } else { + reqHostNoPortNoSubdomain = parts[len(parts)-2] + "." + parts[len(parts)-1] + } + + for _, hostname := range hostnames { + hostnameNoPort := strings.Split(hostname, ":")[0] + hostnameNoPortNoSubdomain := "" + + if parts := strings.Split(hostnameNoPort, "."); len(parts) < 2 { + hostnameNoPortNoSubdomain = hostnameNoPort + } else { + hostnameNoPortNoSubdomain = parts[len(parts)-2] + "." + parts[len(parts)-1] + } + + if reqHostNoPortNoSubdomain == hostnameNoPortNoSubdomain { + return true + } + } + + return false } \ No newline at end of file