[release] version 0.5.0-unstable11

This commit is contained in:
Yann Stepienik 2023-05-16 18:08:01 +01:00
parent e3503f4345
commit 467f84187f
15 changed files with 241 additions and 30 deletions

View file

@ -3,6 +3,7 @@
- Add Create Container - Add Create Container
- Add support for importing Docker Compose - Add support for importing Docker Compose
- Fixed 2 bugs with the smart shield, that made it too strict - 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 - Added more infoon the shield when blocking someone
- Fixed home background image - Fixed home background image

View file

@ -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 { export {
list, list,
get, get,
@ -289,4 +298,5 @@ export {
createTerminal, createTerminal,
createService, createService,
pullImage, pullImage,
autoUpdate,
}; };

View file

@ -1,24 +1,39 @@
import React from 'react'; 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 { CheckCircleOutlined, CloseSquareOutlined, DeleteOutlined, PauseCircleOutlined, PlaySquareOutlined, ReloadOutlined, RollbackOutlined, StopOutlined, UpCircleOutlined } from '@ant-design/icons';
import * as API from '../../api'; import * as API from '../../api';
import LogsInModal from '../../components/logsInModal';
const GetActions = ({ const GetActions = ({
Id, Id,
state, state,
image,
refreshServeApps, refreshServeApps,
setIsUpdatingId, setIsUpdatingId,
updateAvailable updateAvailable
}) => { }) => {
const [confirmDelete, setConfirmDelete] = React.useState(false); const [confirmDelete, setConfirmDelete] = React.useState(false);
const isMiniMobile = useMediaQuery((theme) => theme.breakpoints.down('xsm')); const isMiniMobile = useMediaQuery((theme) => theme.breakpoints.down('xsm'));
const [pullRequest, setPullRequest] = React.useState(null);
const [isUpdating, setIsUpdating] = React.useState(false);
console.log(isMiniMobile) console.log(isMiniMobile)
const doTo = (action) => { const doTo = (action) => {
setIsUpdating(true);
if(action === 'pull') {
setPullRequest(() => ((cb) => {
API.docker.pullImage(image, cb, true)
}));
return;
}
setIsUpdatingId(Id, true); setIsUpdatingId(Id, true);
API.docker.manageContainer(Id, action).then((res) => { return API.docker.manageContainer(Id, action).then((res) => {
setIsUpdating(false);
refreshServeApps(); refreshServeApps();
}).catch((err) => { }).catch((err) => {
setIsUpdating(false);
refreshServeApps(); refreshServeApps();
}); });
}; };
@ -27,7 +42,7 @@ const GetActions = ({
{ {
t: 'Update Available', t: 'Update Available',
if: ['update_available'], if: ['update_available'],
e: <IconButton className="shinyButton" color='primary' onClick={() => {doTo('update')}} size={isMiniMobile ? 'medium' : 'large'}> e: <IconButton className="shinyButton" color='primary' onClick={() => {doTo('pull')}} size={isMiniMobile ? 'medium' : 'large'}>
<UpCircleOutlined /> <UpCircleOutlined />
</IconButton> </IconButton>
}, },
@ -93,11 +108,28 @@ const GetActions = ({
} }
]; ];
return actions.filter((action) => { return <>
return action.if.includes(state) || (updateAvailable && action.if.includes('update_available')); <LogsInModal
}).map((action) => { request={pullRequest}
return <Tooltip title={action.t}>{action.e}</Tooltip> title="Updating ServeApp..."
}); OnSuccess={() => {
doTo('update')
}}
/>
{!isUpdating && actions.filter((action) => {
updateAvailable = true;
return action.if.includes(state) || (updateAvailable && action.if.includes('update_available'));
}).map((action) => {
return <Tooltip title={action.t}>{action.e}</Tooltip>
})}
{isUpdating && <Stack sx={{
width: '100%', height: '44px',
}}
justifyContent={'center'}
><LinearProgress /></Stack>}
</>
} }
export default GetActions; export default GetActions;

View file

@ -91,6 +91,7 @@ const ContainerOverview = ({ containerInfo, config, refresh }) => {
<Stack spacing={2} direction={'row'} > <Stack spacing={2} direction={'row'} >
<GetActions <GetActions
Id={containerInfo.Name} Id={containerInfo.Name}
image={Image}
state={State.Status} state={State.Status}
refreshServeApps={() => { refreshServeApps={() => {
refreshAll() refreshAll()
@ -130,6 +131,20 @@ const ContainerOverview = ({ containerInfo, config, refresh }) => {
}} }}
/> Force Secure Network /> Force Secure Network
</Stack> </Stack>
<Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
<Checkbox
checked={Config.Labels['cosmos-auto-update'] === 'true'}
disabled={isUpdating}
onChange={(e) => {
setIsUpdating(true);
API.docker.autoUpdate(Name, e.target.checked).then(() => {
setTimeout(() => {
refreshAll();
}, 3000);
})
}}
/> Auto Update Container
</Stack>
<strong><NodeExpandOutlined /> URLs</strong> <strong><NodeExpandOutlined /> URLs</strong>
<div> <div>
{routes.map((route) => { {routes.map((route) => {

View file

@ -188,16 +188,14 @@ const ServeApps = () => {
</Stack> </Stack>
</Stack> </Stack>
</Stack> </Stack>
<Stack direction="row" spacing={2}> <Stack direction="row" spacing={2} width='100%'>
{/* <Button variant="contained" size="small" onClick={() => {}}>
Update
</Button> */}
<GetActions <GetActions
Id={app.Names[0].replace('/', '')} Id={app.Names[0].replace('/', '')}
image={app.Image}
state={app.State} state={app.State}
setIsUpdatingId={setIsUpdatingId} setIsUpdatingId={setIsUpdatingId}
refreshServeApps={refreshServeApps} refreshServeApps={refreshServeApps}
updateAvailable={updatesAvailable[app.Names[0]]} updateAvailable={updatesAvailable && updatesAvailable[app.Names[0]]}
/> />
</Stack> </Stack>
</Stack> </Stack>
@ -242,10 +240,31 @@ const ServeApps = () => {
setIsUpdatingId(app.Id, false); setIsUpdatingId(app.Id, false);
refreshServeApps(); refreshServeApps();
}, 3000); }, 3000);
}).catch(() => {
setIsUpdatingId(app.Id, false);
refreshServeApps();
}) })
}} }}
/> Force Secure Network /> Force Secure Network
</Stack> </Stack>
<Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
<Checkbox
checked={app.Labels['cosmos-auto-update'] === 'true'}
disabled={app.State !== 'running'}
onChange={(e) => {
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
</Stack>
</Stack> </Stack>
} }
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start"> <Stack margin={1} direction="column" spacing={1} alignItems="flex-start">

View file

@ -1,6 +1,6 @@
{ {
"name": "cosmos-server", "name": "cosmos-server",
"version": "0.5.0-unstable10", "version": "0.5.0-unstable11",
"description": "", "description": "",
"main": "test-server.js", "main": "test-server.js",
"bugs": { "bugs": {

View file

@ -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
}
}

View file

@ -161,7 +161,7 @@ func CreateServiceRoute(w http.ResponseWriter, req *http.Request) {
errD := Connect() errD := Connect()
if errD != nil { if errD != nil {
utils.Error("CreateService", errD) utils.Error("CreateService - connect - ", errD)
utils.HTTPError(w, "Internal server error: " + errD.Error(), http.StatusInternalServerError, "DS002") utils.HTTPError(w, "Internal server error: " + errD.Error(), http.StatusInternalServerError, "DS002")
return return
} }
@ -191,7 +191,7 @@ func CreateServiceRoute(w http.ResponseWriter, req *http.Request) {
var serviceRequest DockerServiceCreateRequest var serviceRequest DockerServiceCreateRequest
err := decoder.Decode(&serviceRequest) err := decoder.Decode(&serviceRequest)
if err != nil { if err != nil {
utils.Error("CreateService", err) utils.Error("CreateService - decode - ", err)
utils.HTTPError(w, "Bad request: "+err.Error(), http.StatusBadRequest, "DS003") utils.HTTPError(w, "Bad request: "+err.Error(), http.StatusBadRequest, "DS003")
return return
} }

View file

@ -230,6 +230,7 @@ func StartServer() {
srapi.HandleFunc("/api/servapps/{containerId}/manage/{action}", docker.ManageContainerRoute) srapi.HandleFunc("/api/servapps/{containerId}/manage/{action}", docker.ManageContainerRoute)
srapi.HandleFunc("/api/servapps/{containerId}/secure/{status}", docker.SecureContainerRoute) 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}/logs", docker.GetContainerLogsRoute)
srapi.HandleFunc("/api/servapps/{containerId}/terminal/{action}", docker.TerminalRoute) srapi.HandleFunc("/api/servapps/{containerId}/terminal/{action}", docker.TerminalRoute)
srapi.HandleFunc("/api/servapps/{containerId}/update", docker.UpdateContainerRoute) srapi.HandleFunc("/api/servapps/{containerId}/update", docker.UpdateContainerRoute)

View file

@ -24,6 +24,10 @@ func main() {
docker.BootstrapAllContainersFromTags() docker.BootstrapAllContainersFromTags()
// TODO DELET THIS BEFORE RELEASE
docker.CheckUpdatesAvailable()
version, err := docker.DockerClient.ServerVersion(context.Background()) version, err := docker.DockerClient.ServerVersion(context.Background())
if err == nil { if err == nil {
utils.Log("Docker API version: " + version.APIVersion) utils.Log("Docker API version: " + version.APIVersion)

View file

@ -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{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK", "status": "OK",

View file

@ -70,7 +70,7 @@ func UserLogin(w http.ResponseWriter, req *http.Request) {
return return
} }
SendUserToken(w, user, false) SendUserToken(w, req, user, false)
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK", "status": "OK",

View file

@ -10,7 +10,7 @@ func UserLogout(w http.ResponseWriter, req *http.Request) {
if(req.Method == "GET") { if(req.Method == "GET") {
utils.Debug("UserLogout: Logging out user") utils.Debug("UserLogout: Logging out user")
logOutUser(w); logOutUser(w, req);
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK", "status": "OK",

View file

@ -12,7 +12,7 @@ import (
func quickLoggout(w http.ResponseWriter, req *http.Request, err error) (utils.User, error) { func quickLoggout(w http.ResponseWriter, req *http.Request, err error) (utils.User, error) {
utils.Error("UserToken: Token likely falsified", err) utils.Error("UserToken: Token likely falsified", err)
logOutUser(w) logOutUser(w, req)
redirectToReLogin(w, req) redirectToReLogin(w, req)
return utils.User{}, errors.New("Token likely falsified") 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 { if errP != nil {
utils.Error("UserToken: token is not valid", nil) utils.Error("UserToken: token is not valid", nil)
logOutUser(w) logOutUser(w, req)
redirectToReLogin(w, req) redirectToReLogin(w, req)
return utils.User{}, errors.New("Token not valid") 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 passwordCycle int
mfaDone bool mfaDone bool
ok bool ok bool
forDomain string
) )
if nickname, ok = claims["nickname"].(string); !ok { 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{} userInBase := utils.User{}
c, errCo := utils.GetCollection(utils.GetRootAppId(), "users") 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 { if errDB != nil {
utils.Error("UserToken: User not found", errDB) utils.Error("UserToken: User not found", errDB)
logOutUser(w) logOutUser(w, req)
redirectToReLogin(w, req) redirectToReLogin(w, req)
return utils.User{}, errors.New("User not found") return utils.User{}, errors.New("User not found")
} }
if userInBase.PasswordCycle != passwordCycle { if userInBase.PasswordCycle != passwordCycle {
utils.Error("UserToken: Password cycle changed, token is too old", nil) utils.Error("UserToken: Password cycle changed, token is too old", nil)
logOutUser(w) logOutUser(w, req)
redirectToReLogin(w, req) redirectToReLogin(w, req)
return utils.User{}, errors.New("Password cycle changed, token is too old") 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 { if time.Now().Unix() - int64(claims["iat"].(float64)) > 3600 {
SendUserToken(w, userInBase, mfaDone) SendUserToken(w, req, userInBase, mfaDone)
} }
return userInBase, nil 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{ cookie := http.Cookie{
Name: "jwttoken", Name: "jwttoken",
Value: "", Value: "",
@ -167,11 +187,12 @@ func logOutUser(w http.ResponseWriter) {
Path: "/", Path: "/",
Secure: utils.IsHTTPS, Secure: utils.IsHTTPS,
HttpOnly: true, 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 = "" cookie.Domain = ""
} else {
cookie.Domain = "." + reqHostNoPort
} }
http.SetCookie(w, &cookie) 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) 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) expiration := time.Now().Add(3 * 24 * time.Hour)
token := jwt.New(jwt.SigningMethodEdDSA) 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["iat"] = time.Now().Unix()
claims["nbf"] = time.Now().Unix() claims["nbf"] = time.Now().Unix()
claims["mfaDone"] = mfaDone claims["mfaDone"] = mfaDone
claims["forDomain"] = reqHostNoPort
key, err5 := jwt.ParseEdPrivateKeyFromPEM([]byte(utils.GetPrivateAuthKey())) key, err5 := jwt.ParseEdPrivateKeyFromPEM([]byte(utils.GetPrivateAuthKey()))
@ -227,11 +252,20 @@ func SendUserToken(w http.ResponseWriter, user utils.User, mfaDone bool) {
Path: "/", Path: "/",
Secure: utils.IsHTTPS, Secure: utils.IsHTTPS,
HttpOnly: true, 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 = "" 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) http.SetCookie(w, &cookie)

View file

@ -202,4 +202,42 @@ func EnsureHostname(next http.Handler) http.Handler {
next.ServeHTTP(w, r) 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
} }