Bläddra i källkod

[release] v0.10.0-unstable

Yann Stepienik 1 år sedan
förälder
incheckning
bc7aaa21d0

+ 18 - 0
.circleci/config.yml

@@ -53,6 +53,24 @@ jobs:
           command: |
             curl -s -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=$MAX_TOKEN&suffix=tar.gz" -o GeoLite2-Country.tar.gz
             tar -xzf GeoLite2-Country.tar.gz --strip-components 1 --wildcards "*.mmdb"
+      
+      - run:
+          name: Download and Extract ARM Nebula Binary
+          command: |
+            curl -LO https://github.com/slackhq/nebula/releases/download/v1.7.2/nebula-linux-arm64.tar.gz
+            tar -xzvf nebula-linux-arm64.tar.gz
+      
+      - run:
+          name: Rename ARM Nebula Binary
+          command: |
+            mv nebula nebula-arm
+            mv nebula-cert nebula-cert-arm
+      
+      - run:
+          name: Download and Extract Nebula Binary
+          command: |
+            curl -LO https://github.com/slackhq/nebula/releases/download/v1.7.2/nebula-linux-amd64.tar.gz
+            tar -xzvf nebula-linux-amd64.tar.gz
 
       - run:
           name: Build UI

+ 6 - 1
.gitignore

@@ -12,4 +12,9 @@ todo.txt
 LICENCE
 tokens.json
 .vscode
-GeoLite2-Country.mmdb
+GeoLite2-Country.mmdb
+zz_test_config
+nebula-arm
+nebula-arm-cert
+nebula
+nebula-cert

+ 1 - 0
build.sh

@@ -18,6 +18,7 @@ echo " ---- Build complete, copy assets ----"
 
 cp -r static build/
 cp -r GeoLite2-Country.mmdb build/
+cp nebula-arm-cert nebula-cert nebula-arm nebula build/
 cp -r Logo.png build/
 mkdir build/images
 cp client/src/assets/images/icons/cosmos_gray.png build/cosmos_gray.png

+ 6 - 0
changelog.md

@@ -1,6 +1,12 @@
+<<<<<<< HEAD
 ## Version 0.9.20 - 0.9.21
  - Add option to disable CORS hardening (with empty value)
 
+=======
+## Version 0.10.0
+ - Added Constellation
+ - DNS Challenge is now used for all certificates when enabled
+>>>>>>> b8a9e71 ([release] v0.10.0-unstable)
 ## Version 0.9.19
  - Add country whitelist option to geoblocker
  - No countries blocked by default anymore

+ 25 - 0
client/src/api/constellation.tsx

@@ -0,0 +1,25 @@
+import wrap from './wrap';
+
+function list() {
+  return wrap(fetch('/cosmos/api/constellation/devices', {
+    method: 'GET',
+    headers: {
+        'Content-Type': 'application/json'
+    },
+  }))
+}
+
+function addDevice(device) {
+  return wrap(fetch('/cosmos/api/constellation/devices', {
+    method: 'POST',
+    headers: {
+        'Content-Type': 'application/json'
+    },
+    body: JSON.stringify(device),
+  }))
+}
+
+export {
+  list,
+  addDevice,
+};

+ 28 - 0
client/src/api/downloadButton.jsx

@@ -0,0 +1,28 @@
+import { Button } from "@mui/material";
+
+export const DownloadFile = ({ filename, content, label }) => {
+  const downloadFile = () => {
+      // Create a blob with the content
+      const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
+
+      // Create a link element
+      const link = document.createElement("a");
+      link.href = URL.createObjectURL(blob);
+      link.download = filename;
+
+      // Append the link to the document (needed for Firefox)
+      document.body.appendChild(link);
+
+      // Simulate a click to start the download
+      link.click();
+
+      // Cleanup the DOM by removing the link element
+      document.body.removeChild(link);
+  }
+
+  return (
+      <Button onClick={downloadFile}>
+          {label}
+      </Button>
+  );
+}

+ 3 - 0
client/src/api/index.jsx

@@ -3,6 +3,7 @@ import * as _users from './users';
 import * as _config from './config';
 import * as _docker from './docker';
 import * as _market from './market';
+import * as _constellation from './constellation';
 
 import * as authDemo from './authentication.demo';
 import * as usersDemo from './users.demo';
@@ -211,6 +212,7 @@ let users = _users;
 let config = _config;
 let docker = _docker;
 let market = _market;
+let constellation = _constellation;
 
 if(isDemo) {
   auth = authDemo;
@@ -232,6 +234,7 @@ export {
   config,
   docker,
   market,
+  constellation,
   getStatus,
   newInstall,
   isOnline,

BIN
client/src/assets/images/icons/constellation.png


+ 9 - 1
client/src/menu-items/pages.jsx

@@ -1,5 +1,6 @@
 // assets
 import { ProfileOutlined, PicLeftOutlined, SettingOutlined, NodeExpandOutlined, AppstoreOutlined} from '@ant-design/icons';
+import ConstellationIcon from '../assets/images/icons/constellation.png'
 
 // icons
 const icons = {
@@ -7,7 +8,6 @@ const icons = {
     ProfileOutlined,
     SettingOutlined
 };
-
 // ==============================|| MENU ITEMS - EXTRA PAGES ||============================== //
 
 const pages = {
@@ -29,6 +29,14 @@ const pages = {
             url: '/cosmos-ui/config-url',
             icon: icons.NodeExpandOutlined,
         },
+        {
+            id: 'constellation',
+            title: 'Constellation',
+            type: 'item',
+            url: '/cosmos-ui/constellation',
+            icon: () => <img height="28px" width="28px" style={{marginLeft: "-6px"}} src={ConstellationIcon} />,
+            
+        },
         {
             id: 'users',
             title: 'Users',

+ 147 - 0
client/src/pages/constellation/addDevice.jsx

@@ -0,0 +1,147 @@
+// material-ui
+import { Alert, Button, Stack, TextField } from '@mui/material';
+import Dialog from '@mui/material/Dialog';
+import DialogActions from '@mui/material/DialogActions';
+import DialogContent from '@mui/material/DialogContent';
+import DialogContentText from '@mui/material/DialogContentText';
+import DialogTitle from '@mui/material/DialogTitle';
+import * as React from 'react';
+import { useState } from 'react';
+import ResponsiveButton from '../../components/responseiveButton';
+import { PlusCircleFilled } from '@ant-design/icons';
+import { Formik } from 'formik';
+import * as yup from 'yup';
+import * as API from '../../api';
+import { CosmosInputText, CosmosSelect } from '../config/users/formShortcuts';
+import { DownloadFile } from '../../api/downloadButton';
+
+const AddDeviceModal = ({ config, isAdmin, refreshConfig, devices }) => {
+  const [openModal, setOpenModal] = useState(false);
+  const [isDone, setIsDone] = useState(null);
+
+  return <>
+    <Dialog open={openModal} onClose={() => setOpenModal(false)}>
+      <Formik
+        initialValues={{
+          nickname: '',
+          deviceName: '',
+          ip: '192.168.201.1/24',
+          publicKey: '',
+        }}
+
+        validationSchema={yup.object({
+        })}
+
+        onSubmit={(values, { setSubmitting, setStatus, setErrors }) => {
+          return API.constellation.addDevice(values).then(({data}) => {
+            setIsDone(data);
+            refreshConfig();
+          }).catch((err) => {
+            setErrors(err.response.data);
+          });
+        }}
+      >
+        {(formik) => (
+          <form onSubmit={formik.handleSubmit}>
+            <DialogTitle>Manually Add Device</DialogTitle>
+
+            {isDone ? <DialogContent>
+              <DialogContentText>
+                <p>
+                  Device added successfully!
+                  Download the private and public keys to your device along side the config and network certificate to connect:
+                </p>
+
+                <Stack spacing={2} direction={"column"}>
+                  <DownloadFile 
+                    filename={`config.yml`}
+                    content={isDone.Config}
+                    label={"Download config.yml"}
+                  />
+                  <DownloadFile
+                    filename={isDone.DeviceName + `.key`}
+                    content={isDone.PrivateKey}
+                    label={"Download " + isDone.DeviceName + `.key`}
+                  />
+                  <DownloadFile
+                    filename={isDone.DeviceName + `.crt`}
+                    content={isDone.PublicKey}
+                    label={"Download " + isDone.DeviceName + `.crt`}
+                  />
+                  <DownloadFile
+                    filename={`ca.crt`}
+                    content={isDone.CA}
+                    label={"Download ca.crt"}
+                  />
+                </Stack>
+              </DialogContentText>
+            </DialogContent> : <DialogContent>
+              <DialogContentText>
+                <p>Manually add a device to the constellation. It is recommended that you use the Cosmos app instead. Use this form to add another Nebula device manually</p>
+                <div>
+                  <Stack spacing={2} style={{}}>
+                    <CosmosSelect
+                      name="nickname"
+                      label="Owner"
+                      formik={formik}
+                      // disabled={!isAdmin}
+                      options={[
+                        ["admin", "admin"]
+                      ]}
+                    />
+
+                    <CosmosInputText
+                      name="deviceName"
+                      label="Device Name"
+                      formik={formik}
+                    />
+
+                    <CosmosInputText
+                      name="ip"
+                      label="Constellation IP Address"
+                      formik={formik}
+                    />
+
+                    <CosmosInputText
+                      multiline
+                      name="publicKey"
+                      label="Public Key (Optional)"
+                      formik={formik}
+                    />
+                    <div>
+                      {formik.errors && formik.errors.length > 0 && <Stack spacing={2} direction={"column"}>
+                        <Alert severity="error">{formik.errors.map((err) => {
+                          return <div>{err}</div>
+                        })}</Alert>
+                      </Stack>}
+                    </div>
+                  </Stack>
+                </div>
+              </DialogContentText>
+            </DialogContent>}
+
+            <DialogActions>
+              <Button onClick={() => setOpenModal(false)}>Close</Button>
+              <Button color="primary" variant="contained" type="submit">Add</Button>
+            </DialogActions>
+          </form>
+
+        )}
+      </Formik>
+    </Dialog>
+
+    <ResponsiveButton
+      color="primary"
+      onClick={() => {
+        setIsDone(null);
+        setOpenModal(true);
+      }}
+      variant="contained"
+      startIcon={<PlusCircleFilled />}
+    >
+      Manually Add Device
+    </ResponsiveButton>
+  </>;
+};
+
+export default AddDeviceModal;

+ 81 - 0
client/src/pages/constellation/index.jsx

@@ -0,0 +1,81 @@
+import React from "react";
+import { useEffect, useState } from "react";
+import * as API  from "../../api";
+import AddDeviceModal from "./addDevice";
+import PrettyTableView from "../../components/tableView/prettyTableView";
+import { DeleteButton } from "../../components/delete";
+import { CloudOutlined, DesktopOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons";
+import IsLoggedIn from "../../isLoggedIn";
+
+export const ConstellationIndex = () => {
+  const [isAdmin, setIsAdmin] = useState(false);
+  const [config, setConfig] = useState(null);
+  const [devices, setDevices] = useState(null);
+
+  const refreshConfig = async () => {
+    let configAsync = await API.config.get();
+    setConfig(configAsync.data);
+    setIsAdmin(configAsync.isAdmin);
+    setDevices((await API.constellation.list()).data || []);
+  };
+
+  useEffect(() => {
+    refreshConfig();
+  }, []);
+
+  const getIcon = (r) => {
+    if (r.deviceName.toLowerCase().includes("mobile") || r.deviceName.toLowerCase().includes("phone")) {
+      return <MobileOutlined />
+    }
+    else if (r.deviceName.toLowerCase().includes("laptop") || r.deviceName.toLowerCase().includes("computer")) {
+      return <LaptopOutlined />
+    } else if (r.deviceName.toLowerCase().includes("desktop")) {
+      return <DesktopOutlined />
+    } else if (r.deviceName.toLowerCase().includes("tablet")) {
+      return <TabletOutlined />
+    } else {
+      return <CloudOutlined />
+    }
+  }
+
+  return <>
+    <IsLoggedIn />
+    {devices && config && <>
+      <PrettyTableView 
+            data={devices}
+            getKey={(r) => r.deviceName}
+            buttons={[
+              <AddDeviceModal isAdmin={isAdmin} config={config} refreshConfig={refreshConfig} devices={devices} />
+            ]}
+            columns={[
+                {
+                    title: '',
+                    field: getIcon,
+                },
+                {
+                    title: 'Device Name',
+                    field: (r) => <strong>{r.deviceName}</strong>,
+                },
+                {
+                    title: 'Owner',
+                    field: (r) => <strong>{r.nickname}</strong>,
+                },
+                {
+                    title: 'Constellation IP',
+                    screenMin: 'md', 
+                    field: (r) => r.ip,
+                },
+                {
+                  title: '',
+                  clickable: true,
+                  field: (r) => {
+                    return <DeleteButton onDelete={async () => {
+                      alert("caca")
+                    }}></DeleteButton>
+                  }
+                }
+            ]}
+        />
+    </>}
+  </>
+};

+ 5 - 0
client/src/routes/MainRoutes.jsx

@@ -15,6 +15,7 @@ import ContainerIndex from '../pages/servapps/containers';
 import NewDockerServiceForm from '../pages/servapps/containers/newServiceForm';
 import OpenIdList from '../pages/openid/openid-list';
 import MarketPage from '../pages/market/listing';
+import { ConstellationIndex } from '../pages/constellation';
 
 
 // render - dashboard
@@ -44,6 +45,10 @@ const MainRoutes = {
             path: '/cosmos-ui/dashboard',
             element: <DashboardDefault />
         },
+        {
+            path: '/cosmos-ui/constellation',
+            element: <ConstellationIndex />
+        },
         {
             path: '/cosmos-ui/servapps',
             element: <ServAppsIndex />

+ 3 - 1
client/src/utils/indexs.js

@@ -1,3 +1,5 @@
+import { Button } from "@mui/material";
+
 export const randomString = (length) => {
   let text = "";
   const possible =
@@ -45,4 +47,4 @@ export const redirectToLocal = (url) => {
     throw new Error("URL must be local");
   }
   window.location.href = url;
-}
+}

+ 1 - 1
dockerfile

@@ -29,7 +29,7 @@ WORKDIR /app
 COPY build/cosmos build/cosmos-arm64 ./
 
 # Copy other resources
-COPY build/cosmos_gray.png build/Logo.png build/GeoLite2-Country.mmdb build/meta.json ./
+COPY build/* ./
 COPY static ./static
 
 # Run the respective binary based on the BINARY_NAME

+ 2 - 1
dockerfile.arm64

@@ -13,7 +13,8 @@ RUN apt-get update \
 
 WORKDIR /app
 
-COPY build/cosmos build/cosmos_gray.png build/Logo.png build/GeoLite2-Country.mmdb build/meta.json ./
+
+COPY build/* ./
 COPY static ./static
 
 CMD ["./cosmos"]

+ 4 - 5
package.json

@@ -1,6 +1,6 @@
 {
   "name": "cosmos-server",
-  "version": "0.9.21",
+  "version": "0.10.0-unstable",
   "description": "",
   "main": "test-server.js",
   "bugs": {
@@ -63,13 +63,12 @@
   "scripts": {
     "client": "vite",
     "client-build": "vite build --base=/cosmos-ui/",
-    "start": "env CONFIG_FILE=./config_dev.json EZ=UTC ACME_STAGING=true build/cosmos",
+    "start": "env COSMOS_CONFIG_FOLDER=/mnt/e/work/Cosmos-Server/zz_test_config/ CONFIG_FILE=./config_dev.json EZ=UTC ACME_STAGING=true build/cosmos",
     "build": "sh build.sh",
     "dev": "npm run build && npm run start",
     "dockerdevbuild": "sh build.sh && docker build -f dockerfile.local --tag cosmos-dev .",
-    "dockerdevrun": "docker stop cosmos-dev; docker rm cosmos-dev; docker run -d -p 7200:443 -p 80:80 -p 443:443 -e DOCKER_HOST=tcp://host.docker.internal:2375 -e COSMOS_MONGODB=$MONGODB -e COSMOS_LOG_LEVEL=DEBUG -v /:/mnt/host  --restart=unless-stopped -h cosmos-dev --name cosmos-dev cosmos-dev",
-    "dockerdev": "npm run dockerdevbuild && npm run dockerdevrun",
-    "dockerdevclient": "npm run client-build && npm run dockerdevbuild && npm run dockerdevrun",
+    "dockerdevrun": "docker stop cosmos-dev; docker rm cosmos-dev; docker run --cap-add NET_ADMIN -d -p 7200:443 -p 80:80 -p 443:443 -p 4242:4242 -e DOCKER_HOST=tcp://host.docker.internal:2375 -e COSMOS_MONGODB=$MONGODB -e COSMOS_LOG_LEVEL=DEBUG -v /:/mnt/host  --restart=unless-stopped -h cosmos-dev --name cosmos-dev cosmos-dev",
+    "dockerdev": "npm run client-build && npm run dockerdevbuild && npm run dockerdevrun",
     "demo": "vite build --base=/cosmos-ui/ --mode demo",
     "devdemo": "vite --mode demo"
   },

+ 2 - 2
src/background.go

@@ -54,7 +54,7 @@ func UploadBackground(w http.ResponseWriter, req *http.Request) {
 		}
 
 		// create a new file in the config directory
-		dst, err := os.Create("/config/background" + ext)
+		dst, err := os.Create(utils.CONFIGFOLDER + "background" + ext)
 		if err != nil {
 			utils.HTTPError(w, "Error creating destination file", http.StatusInternalServerError, "FILE004")
 			return
@@ -99,7 +99,7 @@ func GetBackground(w http.ResponseWriter, req *http.Request) {
 
 	if(req.Method == "GET") {
 		// get the background image
-		bg, err := ioutil.ReadFile("/config/background." + ext)
+		bg, err := ioutil.ReadFile(utils.CONFIGFOLDER + "background." + ext)
 		if err != nil {
 			utils.HTTPError(w, "Error reading background image", http.StatusInternalServerError, "FILE003")
 			return

+ 1 - 0
src/configapi/get.go

@@ -42,6 +42,7 @@ func ConfigApiGet(w http.ResponseWriter, req *http.Request) {
 			"data": config,
 			"updates": utils.UpdateAvailable,
 			"hostname": os.Getenv("HOSTNAME"),
+			"isAdmin": isAdmin,
 		})
 	} else {
 		utils.Error("SettingGet: Method not allowed" + req.Method, nil)

+ 129 - 0
src/constellation/api_devices_create.go

@@ -0,0 +1,129 @@
+package constellation
+
+import (
+	"net/http"
+	"encoding/json"
+	"go.mongodb.org/mongo-driver/mongo"
+	
+	"github.com/azukaar/cosmos-server/src/utils" 
+)
+
+type DeviceCreateRequestJSON struct {
+	Nickname string `json:"nickname",validate:"required,min=3,max=32,alphanum"`
+	DeviceName string `json:"deviceName",validate:"required,min=3,max=32,alphanum"`
+	IP string `json:"ip",validate:"required,ipv4"`
+	PublicKey string `json:"publicKey",omitempty`
+}
+
+func DeviceCreate(w http.ResponseWriter, req *http.Request) {
+	
+	if(req.Method == "POST") {
+		var request DeviceCreateRequestJSON
+		err1 := json.NewDecoder(req.Body).Decode(&request)
+		if err1 != nil {
+			utils.Error("ConstellationDeviceCreation: Invalid User Request", err1)
+			utils.HTTPError(w, "Device Creation Error",
+				http.StatusInternalServerError, "DC001")
+			return 
+		}
+
+		errV := utils.Validate.Struct(request)
+		if errV != nil {
+			utils.Error("DeviceCreation: Invalid User Request", errV)
+			utils.HTTPError(w, "Device Creation Error: " + errV.Error(),
+				http.StatusInternalServerError, "DC002")
+			return 
+		}
+		
+		nickname := utils.Sanitize(request.Nickname)
+		deviceName := utils.Sanitize(request.DeviceName)
+		
+		if utils.AdminOrItselfOnly(w, req, nickname) != nil {
+			return
+		}
+
+		c, errCo := utils.GetCollection(utils.GetRootAppId(), "devices")
+		if errCo != nil {
+				utils.Error("Database Connect", errCo)
+				utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
+				return
+		}
+
+		device := utils.Device{}
+
+		utils.Debug("ConstellationDeviceCreation: Creating Device " + deviceName)
+		
+		err2 := c.FindOne(nil, map[string]interface{}{
+			"DeviceName": deviceName,
+		}).Decode(&device)
+
+		if err2 == mongo.ErrNoDocuments {
+			cert, key, err := generateNebulaCert(deviceName, request.IP, false)
+
+			if err != nil {
+				utils.Error("DeviceCreation: Error while creating Device", err)
+				utils.HTTPError(w, "Device Creation Error: " + err.Error(),
+					http.StatusInternalServerError, "DC001")
+				return
+			}
+
+			_, err3 := c.InsertOne(nil, map[string]interface{}{
+				"Nickname": nickname,
+				"DeviceName": deviceName,
+				"PublicKey": cert,
+				"PrivateKey": key,
+				"IP": request.IP,
+			})
+
+			if err3 != nil {
+				utils.Error("DeviceCreation: Error while creating Device", err3)
+				utils.HTTPError(w, "Device Creation Error: " + err.Error(),
+					http.StatusInternalServerError, "DC004")
+				return 
+			} 
+
+			// read configYml from config/nebula.yml
+			configYml, err := getYAMLClientConfig(deviceName, utils.CONFIGFOLDER + "nebula.yml")
+			if err != nil {
+				utils.Error("DeviceCreation: Error while reading config", err)
+				utils.HTTPError(w, "Device Creation Error: " + err.Error(),
+					http.StatusInternalServerError, "DC005")
+				return
+			}
+
+			capki, err := getCApki()
+			if err != nil {
+				utils.Error("DeviceCreation: Error while reading ca.crt", err)
+				utils.HTTPError(w, "Device Creation Error: " + err.Error(),
+					http.StatusInternalServerError, "DC006")
+				return
+			}
+			
+			json.NewEncoder(w).Encode(map[string]interface{}{
+				"status": "OK",
+				"data": map[string]interface{}{
+					"Nickname": nickname,
+					"DeviceName": deviceName,
+					"PublicKey": cert,
+					"PrivateKey": key,
+					"IP": request.IP,
+					"Config": configYml,
+					"CA": capki,
+				},
+			})
+		} else if err2 == nil {
+			utils.Error("DeviceCreation: Device already exists", nil)
+			utils.HTTPError(w, "Device name already exists", http.StatusConflict, "DC002")
+		  return 
+		} else {
+			utils.Error("DeviceCreation: Error while finding device", err2)
+			utils.HTTPError(w, "Device Creation Error: " + err2.Error(),
+				 http.StatusInternalServerError, "DC001")
+			return 
+		}
+	} else {
+		utils.Error("DeviceCreation: Method not allowed" + req.Method, nil)
+		utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
+		return
+	}
+}

+ 18 - 0
src/constellation/api_devices_index.go

@@ -0,0 +1,18 @@
+package constellation
+
+import (
+	"net/http"
+	"github.com/azukaar/cosmos-server/src/utils" 
+)
+
+func ConstellationAPIDevices(w http.ResponseWriter, req *http.Request) {
+	if (req.Method == "GET") {
+		DeviceList(w, req)
+	} else if (req.Method == "POST") {
+		DeviceCreate(w, req)
+	} else {
+		utils.Error("UserRoute: Method not allowed" + req.Method, nil)
+		utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
+		return
+	}
+}

+ 83 - 0
src/constellation/api_devices_list.go

@@ -0,0 +1,83 @@
+package constellation
+
+import (
+	"net/http"
+	"encoding/json"
+
+	
+	"github.com/azukaar/cosmos-server/src/utils" 
+)
+
+func DeviceList(w http.ResponseWriter, req *http.Request) {
+	// Check for GET method
+	if req.Method != "GET" {
+		utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP002")
+		return
+	}
+
+	if utils.LoggedInOnly(w, req) != nil {
+		return
+	}
+
+	isAdmin := utils.IsAdmin(req)
+	
+	// Connect to the collection
+	c, errCo := utils.GetCollection(utils.GetRootAppId(), "devices")
+	if errCo != nil {
+		utils.Error("Database Connect", errCo)
+		utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
+		return
+	}
+	
+	var devices []utils.Device
+	
+	// Check if user is an admin
+	if isAdmin {
+		// If admin, get all devices
+		cursor, err := c.Find(nil, map[string]interface{}{})
+		if err != nil {
+			utils.Error("DeviceList: Error fetching devices", err)
+			utils.HTTPError(w, "Error fetching devices", http.StatusInternalServerError, "DL001")
+			return
+		}
+		defer cursor.Close(nil)
+		
+		if err = cursor.All(nil, &devices); err != nil {
+			utils.Error("DeviceList: Error decoding devices", err)
+			utils.HTTPError(w, "Error decoding devices", http.StatusInternalServerError, "DL002")
+			return
+		}
+
+		// Remove the private key from the response
+		for i := range devices {
+			devices[i].PrivateKey = ""
+		}
+	} else {
+		// If not admin, get user's devices based on their nickname
+		nickname := req.Header.Get("x-cosmos-user")
+		cursor, err := c.Find(nil, map[string]interface{}{"Nickname": nickname})
+		if err != nil {
+			utils.Error("DeviceList: Error fetching devices", err)
+			utils.HTTPError(w, "Error fetching devices", http.StatusInternalServerError, "DL003")
+			return
+		}
+		defer cursor.Close(nil)
+		
+		if err = cursor.All(nil, &devices); err != nil {
+			utils.Error("DeviceList: Error decoding devices", err)
+			utils.HTTPError(w, "Error decoding devices", http.StatusInternalServerError, "DL004")
+			return
+		}
+		
+		// Remove the private key from the response
+		for i := range devices {
+			devices[i].PrivateKey = ""
+		}
+	}
+	
+	// Respond with the list of devices
+	json.NewEncoder(w).Encode(map[string]interface{}{
+		"status":  "OK",
+		"data": devices,
+	})
+}

+ 42 - 0
src/constellation/index.go

@@ -0,0 +1,42 @@
+package constellation
+
+import (
+	"github.com/azukaar/cosmos-server/src/utils" 
+	"os"
+)
+
+func Init() {
+	// if Constellation is enabled
+	if utils.GetMainConfig().ConstellationConfig.Enabled {
+		InitConfig()
+		
+		utils.Log("Initializing Constellation module...")
+
+		// check if ca.crt exists
+		if _, err := os.Stat(utils.CONFIGFOLDER + "ca.crt"); os.IsNotExist(err) {
+			utils.Log("Constellation: ca.crt not found, generating...")
+			// generate ca.crt
+			generateNebulaCACert("Cosmos - " + utils.GetMainConfig().HTTPConfig.Hostname)
+		}
+
+		// check if cosmos.crt exists
+		if _, err := os.Stat(utils.CONFIGFOLDER + "cosmos.crt"); os.IsNotExist(err) {
+			utils.Log("Constellation: cosmos.crt not found, generating...")
+			// generate cosmos.crt
+			generateNebulaCert("cosmos", "192.168.201.0/24", true)
+		}
+
+		// export nebula.yml
+		utils.Log("Constellation: exporting nebula.yml...")
+		ExportConfigToYAML(utils.GetMainConfig().ConstellationConfig, utils.CONFIGFOLDER + "nebula.yml")
+
+		// start nebula
+		utils.Log("Constellation: starting nebula...")
+		err := startNebulaInBackground()
+		if err != nil {
+			utils.Error("Constellation: error while starting nebula", err)
+		}
+
+		utils.Log("Constellation module initialized")
+	}
+}

+ 283 - 0
src/constellation/nebula.go

@@ -0,0 +1,283 @@
+package constellation
+
+import (
+	"github.com/azukaar/cosmos-server/src/utils" 
+	"os/exec"
+	"os"
+	"fmt"
+	"errors"
+	"runtime"
+	"sync"
+	"gopkg.in/yaml.v2"
+	"strings"
+	"io/ioutil"
+	"strconv"
+)
+
+var (
+	process    *exec.Cmd
+	processMux sync.Mutex
+)
+
+func binaryToRun() string {
+	if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" {
+		return "./nebula-arm"
+	}
+	return "./nebula"
+}
+
+func startNebulaInBackground() error {
+	processMux.Lock()
+	defer processMux.Unlock()
+
+	if process != nil {
+		return errors.New("nebula is already running")
+	}
+
+	process = exec.Command(binaryToRun(), "-config", utils.CONFIGFOLDER + "nebula.yml")
+
+	process.Stderr = os.Stderr
+	
+	if utils.LoggingLevelLabels[utils.GetMainConfig().LoggingLevel] == utils.DEBUG {
+		process.Stdout = os.Stdout
+	} else {
+		process.Stdout = nil
+	}
+
+	// Start the process in the background
+	if err := process.Start(); err != nil {
+		return err
+	}
+
+	utils.Log(fmt.Sprintf("%s started with PID %d\n", binaryToRun(), process.Process.Pid))
+	return nil
+}
+
+func stop() error {
+	processMux.Lock()
+	defer processMux.Unlock()
+
+	if process == nil {
+		return errors.New("nebula is not running")
+	}
+
+	if err := process.Process.Kill(); err != nil {
+		return err
+	}
+	process = nil
+	utils.Log("Stopped nebula.")
+	return nil
+}
+
+func restart() error {
+	if err := stop(); err != nil {
+		return err
+	}
+	return startNebulaInBackground()
+}
+
+func ExportConfigToYAML(overwriteConfig utils.ConstellationConfig, outputPath string) error {
+	// Combine defaultConfig and overwriteConfig
+	finalConfig := NebulaDefaultConfig
+
+	finalConfig.StaticHostMap = map[string][]string{
+		"192.168.201.0": []string{utils.GetMainConfig().HTTPConfig.Hostname + ":4242"},
+	}
+
+	// Marshal the combined config to YAML
+	yamlData, err := yaml.Marshal(finalConfig)
+	if err != nil {
+		return err
+	}
+
+	// Write YAML data to the specified file
+	yamlFile, err := os.Create(outputPath)
+	if err != nil {
+		return err
+	}
+	defer yamlFile.Close()
+
+	_, err = yamlFile.Write(yamlData)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func getYAMLClientConfig(name, configPath string) (string, error) {
+	utils.Log("Exporting YAML config for " + name + " with file " + configPath)
+
+	// Read the YAML config file
+	yamlData, err := ioutil.ReadFile(configPath)
+	if err != nil {
+		return "", err
+	}
+	
+	// Unmarshal the YAML data into a map interface
+	var configMap map[string]interface{}
+	err = yaml.Unmarshal(yamlData, &configMap)
+	if err != nil {
+		return "", err
+	}
+
+	// set lightHouse to false
+	if lighthouseMap, ok := configMap["lighthouse"].(map[interface{}]interface{}); ok {
+		lighthouseMap["am_lighthouse"] = false
+
+		lighthouseMap["hosts"] = []string{
+			"192.168.201.0",
+		}
+	} else {
+		return "", errors.New("lighthouse not found in nebula.yml")
+	}
+
+	if pkiMap, ok := configMap["pki"].(map[interface{}]interface{}); ok {
+		pkiMap["ca"] = "ca.crt"
+		pkiMap["cert"] = name + ".crt"
+		pkiMap["key"] = name + ".key"
+	} else {
+		return "", errors.New("pki not found in nebula.yml")
+	}
+
+	// export configMap as YML
+	yamlData, err = yaml.Marshal(configMap)
+	if err != nil {
+		return "", err
+	}
+
+	return string(yamlData), nil
+}
+
+func getCApki() (string, error) {
+	// read config/ca.crt
+	caCrt, err := ioutil.ReadFile(utils.CONFIGFOLDER + "ca.crt")
+	if err != nil {
+		return "", err
+	}
+
+	return string(caCrt), nil
+}
+
+func killAllNebulaInstances() error {
+	processMux.Lock()
+	defer processMux.Unlock()
+
+	cmd := exec.Command("ps", "-e", "-o", "pid,command")
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		return err
+	}
+
+	lines := strings.Split(string(output), "\n")
+	for _, line := range lines {
+		if strings.Contains(line, binaryToRun()) {
+			fields := strings.Fields(line)
+			if len(fields) > 1 {
+				pid := fields[0]
+				pidInt, _ := strconv.Atoi(pid)
+				process, err := os.FindProcess(pidInt)
+				if err != nil {
+					return err
+				}
+				err = process.Kill()
+				if err != nil {
+					return err
+				}
+				utils.Log(fmt.Sprintf("Killed Nebula instance with PID %s\n", pid))
+			}
+		}
+	}
+
+	return nil
+}
+
+func generateNebulaCert(name, ip string, saveToFile bool) (string, string, error) {
+	// Run the nebula-cert command
+	cmd := exec.Command(binaryToRun() + "-cert",
+		 "sign",
+		 "-ca-crt", utils.CONFIGFOLDER + "ca.crt",
+		 "-ca-key", utils.CONFIGFOLDER + "ca.key",
+		 "-name", name,
+		 "-ip", ip,
+	)
+	utils.Debug(cmd.String())
+
+	cmd.Stderr = os.Stderr
+	
+	if utils.LoggingLevelLabels[utils.GetMainConfig().LoggingLevel] == utils.DEBUG {
+		cmd.Stdout = os.Stdout
+	} else {
+		cmd.Stdout = nil
+	}
+	
+	cmd.Run()
+
+	if cmd.ProcessState.ExitCode() != 0 {
+		return "", "", fmt.Errorf("nebula-cert exited with an error, check the Cosmos logs")
+	}
+
+	// Read the generated certificate and key files
+	certPath := fmt.Sprintf("./%s.crt", name)
+	keyPath := fmt.Sprintf("./%s.key", name)
+
+	utils.Debug("Reading certificate from " + certPath)
+	utils.Debug("Reading key from " + keyPath)
+
+	certContent, errCert := ioutil.ReadFile(certPath)
+	if errCert != nil {
+		return "", "", fmt.Errorf("failed to read certificate file: %s", errCert)
+	}
+
+	keyContent, errKey := ioutil.ReadFile(keyPath)
+	if errKey != nil {
+		return "", "", fmt.Errorf("failed to read key file: %s", errKey)
+	}
+
+	if saveToFile {
+		cmd = exec.Command("mv", certPath, utils.CONFIGFOLDER + name + ".crt")
+		utils.Debug(cmd.String())
+		cmd.Run()
+		cmd = exec.Command("mv", keyPath, utils.CONFIGFOLDER + name + ".key")
+		utils.Debug(cmd.String())
+		cmd.Run()
+	} else {
+		// Delete the generated certificate and key files
+		if err := os.Remove(certPath); err != nil {
+			return "", "", fmt.Errorf("failed to delete certificate file: %s", err)
+		}
+
+		if err := os.Remove(keyPath); err != nil {
+			return "", "", fmt.Errorf("failed to delete key file: %s", err)
+		}
+	}
+
+	return string(certContent), string(keyContent), nil
+}
+
+func generateNebulaCACert(name string) (error) {
+	// Run the nebula-cert command to generate CA certificate and key
+	cmd := exec.Command(binaryToRun() + "-cert", "ca", "-name", "\""+name+"\"")
+
+	utils.Debug(cmd.String())
+
+	cmd.Stderr = os.Stderr
+	
+	if utils.LoggingLevelLabels[utils.GetMainConfig().LoggingLevel] == utils.DEBUG {
+		cmd.Stdout = os.Stdout
+	} else {
+		cmd.Stdout = nil
+	}
+
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("nebula-cert error: %s", err)
+	}
+	
+	// copy to /config/ca.*
+	cmd = exec.Command("mv", "./ca.crt", utils.CONFIGFOLDER + "ca.crt")
+	cmd.Run()
+	cmd = exec.Command("mv", "./ca.key", utils.CONFIGFOLDER + "ca.key")
+	cmd.Run()
+
+	return nil
+}

+ 95 - 0
src/constellation/nebula_default.go

@@ -0,0 +1,95 @@
+package constellation
+
+import (
+	"github.com/azukaar/cosmos-server/src/utils" 
+)
+
+var NebulaDefaultConfig utils.NebulaConfig
+
+func InitConfig() {
+	NebulaDefaultConfig = utils.NebulaConfig {
+		PKI: struct {
+			CA   string `yaml:"ca"`
+			Cert string `yaml:"cert"`
+			Key  string `yaml:"key"`
+		}{
+			CA:   utils.CONFIGFOLDER + "ca.crt",
+			Cert: utils.CONFIGFOLDER + "cosmos.crt",
+			Key:  utils.CONFIGFOLDER + "cosmos.key",
+		},
+		StaticHostMap: map[string][]string{
+			
+		},
+		Lighthouse: struct {
+			AMLighthouse bool     `yaml:"am_lighthouse"`
+			Interval     int      `yaml:"interval"`
+			Hosts        []string `yaml:"hosts"`
+		}{
+			AMLighthouse: true,
+			Interval:     60,
+			Hosts:        []string{},
+		},
+		Listen: struct {
+			Host string `yaml:"host"`
+			Port int    `yaml:"port"`
+		}{
+			Host: "0.0.0.0",
+			Port: 4242,
+		},
+		Punchy: struct {
+			Punch bool `yaml:"punch"`
+		}{
+			Punch: true,
+		},
+		Relay: struct {
+			AMRelay   bool `yaml:"am_relay"`
+			UseRelays bool `yaml:"use_relays"`
+		}{
+			AMRelay:   false,
+			UseRelays: true,
+		},
+		TUN: struct {
+			Disabled            bool     `yaml:"disabled"`
+			Dev                 string   `yaml:"dev"`
+			DropLocalBroadcast bool     `yaml:"drop_local_broadcast"`
+			DropMulticast       bool     `yaml:"drop_multicast"`
+			TxQueue             int      `yaml:"tx_queue"`
+			MTU                 int      `yaml:"mtu"`
+			Routes              []string `yaml:"routes"`
+			UnsafeRoutes        []string `yaml:"unsafe_routes"`
+		}{
+			Disabled:            false,
+			Dev:                 "nebula1",
+			DropLocalBroadcast: false,
+			DropMulticast:       false,
+			TxQueue:             500,
+			MTU:                 1300,
+			Routes:              nil,
+			UnsafeRoutes:        nil,
+		},
+		Logging: struct {
+			Level  string `yaml:"level"`
+			Format string `yaml:"format"`
+		}{
+			Level:  "info",
+			Format: "text",
+		},
+		Firewall: struct {
+			OutboundAction string                    `yaml:"outbound_action"`
+			InboundAction  string                    `yaml:"inbound_action"`
+			Conntrack      utils.NebulaConntrackConfig `yaml:"conntrack"`
+			Outbound       []utils.NebulaFirewallRule  `yaml:"outbound"`
+			Inbound        []utils.NebulaFirewallRule  `yaml:"inbound"`
+		}{
+			OutboundAction: "drop",
+			InboundAction:  "drop",
+			Conntrack: utils.NebulaConntrackConfig{
+				TCPTimeout:     "12m",
+				UDPTimeout:     "3m",
+				DefaultTimeout: "10m",
+			},
+			Outbound: nil,
+			Inbound:  nil,
+		},
+	}
+}

+ 2 - 0
src/httpServer.go

@@ -9,6 +9,7 @@ import (
 		"github.com/azukaar/cosmos-server/src/docker"
 		"github.com/azukaar/cosmos-server/src/authorizationserver"
 		"github.com/azukaar/cosmos-server/src/market"
+		"github.com/azukaar/cosmos-server/src/constellation"
 		"github.com/gorilla/mux"
 		"strconv"
 		"time"
@@ -331,6 +332,7 @@ func InitServer() *mux.Router {
 	srapi.HandleFunc("/api/background", UploadBackground)
 	srapi.HandleFunc("/api/background/{ext}", GetBackground)
 
+	srapi.HandleFunc("/api/constellation/devices", constellation.ConstellationAPIDevices)
 
 	if(!config.HTTPConfig.AcceptAllInsecureHostname) {
 		srapi.Use(utils.EnsureHostname)

+ 3 - 0
src/index.go

@@ -9,6 +9,7 @@ import (
 	"github.com/azukaar/cosmos-server/src/utils"
 	"github.com/azukaar/cosmos-server/src/authorizationserver"
 	"github.com/azukaar/cosmos-server/src/market"
+	"github.com/azukaar/cosmos-server/src/constellation"
 )
 
 func main() {
@@ -44,5 +45,7 @@ func main() {
 	
 	authorizationserver.Init()
 
+	constellation.Init()
+
 	StartServer()
 }

+ 0 - 3
src/user/create.go

@@ -2,12 +2,9 @@ package user
 
 import (
 	"net/http"
-	// "io"
-	// "os"
 	"encoding/json"
 	"go.mongodb.org/mongo-driver/mongo"
 	"time"
-	// "golang.org/x/crypto/bcrypt"
 
 	"github.com/azukaar/cosmos-server/src/utils" 
 )

+ 13 - 13
src/utils/certificates.go

@@ -181,20 +181,20 @@ func DoLetsEncrypt() (string, string) {
 		}
 
 		err = client.Challenge.SetDNS01Provider(provider)
-	}
-
-	err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", config.HTTPConfig.HTTPPort))
-	if err != nil {
-		Error("LETSENCRYPT_HTTP01", err)
-		LetsEncryptErrors = append(LetsEncryptErrors, err.Error())
-		return "", ""
-	}
+	} else {
+		err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", config.HTTPConfig.HTTPPort))
+		if err != nil {
+			Error("LETSENCRYPT_HTTP01", err)
+			LetsEncryptErrors = append(LetsEncryptErrors, err.Error())
+			return "", ""
+		}
 
-	err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", config.HTTPConfig.HTTPSPort))
-	if err != nil {
-		Error("LETSENCRYPT_TLS01", err)
-		LetsEncryptErrors = append(LetsEncryptErrors, err.Error())
-		return "", ""
+		err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", config.HTTPConfig.HTTPSPort))
+		if err != nil {
+			Error("LETSENCRYPT_TLS01", err)
+			LetsEncryptErrors = append(LetsEncryptErrors, err.Error())
+			return "", ""
+		}
 	}
 
 	// New users will need to register

+ 82 - 1
src/utils/types.go

@@ -90,6 +90,7 @@ type Config struct {
 	MarketConfig MarketConfig
 	HomepageConfig HomepageConfig
 	ThemeConfig ThemeConfig
+	ConstellationConfig ConstellationConfig
 }
 
 type HomepageConfig struct {
@@ -205,4 +206,84 @@ type MarketConfig struct {
 type MarketSource struct {
 	Name string
 	Url string
-}
+}
+
+type ConstellationConfig struct {
+	Enabled bool
+	NebulaConfig NebulaConfig
+}
+
+type NebulaFirewallRule struct {
+	Port   string   `yaml:"port"`
+	Proto  string   `yaml:"proto"`
+	Host   string   `yaml:"host"`
+	Groups []string `yaml:"groups,omitempty"omitempty"`
+}
+
+type NebulaConntrackConfig struct {
+	TCPTimeout     string `yaml:"tcp_timeout"`
+	UDPTimeout     string `yaml:"udp_timeout"`
+	DefaultTimeout string `yaml:"default_timeout"`
+}
+
+type NebulaConfig struct {
+	PKI struct {
+		CA   string `yaml:"ca"`
+		Cert string `yaml:"cert"`
+		Key  string `yaml:"key"`
+	} `yaml:"pki"`
+
+	StaticHostMap map[string][]string `yaml:"static_host_map"`
+
+	Lighthouse struct {
+		AMLighthouse bool     `yaml:"am_lighthouse"`
+		Interval     int      `yaml:"interval"`
+		Hosts        []string `yaml:"hosts"`
+	} `yaml:"lighthouse"`
+
+	Listen struct {
+		Host string `yaml:"host"`
+		Port int    `yaml:"port"`
+	} `yaml:"listen"`
+
+	Punchy struct {
+		Punch bool `yaml:"punch"`
+	} `yaml:"punchy"`
+
+	Relay struct {
+		AMRelay   bool `yaml:"am_relay"`
+		UseRelays bool `yaml:"use_relays"`
+	} `yaml:"relay"`
+
+	TUN struct {
+		Disabled            bool     `yaml:"disabled"`
+		Dev                 string   `yaml:"dev"`
+		DropLocalBroadcast bool     `yaml:"drop_local_broadcast"`
+		DropMulticast       bool     `yaml:"drop_multicast"`
+		TxQueue             int      `yaml:"tx_queue"`
+		MTU                 int      `yaml:"mtu"`
+		Routes              []string `yaml:"routes"`
+		UnsafeRoutes        []string `yaml:"unsafe_routes"`
+	} `yaml:"tun"`
+
+	Logging struct {
+		Level  string `yaml:"level"`
+		Format string `yaml:"format"`
+	} `yaml:"logging"`
+
+	Firewall struct {
+		OutboundAction string                    `yaml:"outbound_action"`
+		InboundAction  string                    `yaml:"inbound_action"`
+		Conntrack      NebulaConntrackConfig `yaml:"conntrack"`
+		Outbound       []NebulaFirewallRule  `yaml:"outbound"`
+		Inbound        []NebulaFirewallRule  `yaml:"inbound"`
+	} `yaml:"firewall"`
+}
+
+type Device struct {
+	DeviceName string `json:"deviceName",validate:"required,min=3,max=32,alphanum"`
+	Nickname string `json:"nickname",validate:"required,min=3,max=32,alphanum"`
+	PublicKey string `json:"publicKey",omitempty`
+	PrivateKey string `json:"privateKey",omitempty`
+	IP string `json:"ip",validate:"required,ipv4"`
+}

+ 7 - 1
src/utils/utils.go

@@ -37,6 +37,8 @@ var ReBootstrapContainer func(string) error
 
 var LetsEncryptErrors = []string{}
 
+var CONFIGFOLDER = "/config/"
+
 var DefaultConfig = Config{
 	LoggingLevel: "INFO",
 	NewInstall:   true,
@@ -193,6 +195,10 @@ func LoadBaseMainConfig(config Config) {
 	if os.Getenv("COSMOS_SERVER_COUNTRY") != "" {
 		MainConfig.ServerCountry = os.Getenv("COSMOS_SERVER_COUNTRY")
 	}
+	if os.Getenv("COSMOS_CONFIG_FOLDER") != "" {
+		Log("Overwriting config folder with " + os.Getenv("COSMOS_CONFIG_FOLDER"))
+		CONFIGFOLDER = os.Getenv("COSMOS_CONFIG_FOLDER")
+	}
 	
 	if MainConfig.DockerConfig.DefaultDataPath == "" {
 		MainConfig.DockerConfig.DefaultDataPath = "/usr"
@@ -219,7 +225,7 @@ func GetConfigFileName() string {
 	configFile := os.Getenv("CONFIG_FILE")
 
 	if configFile == "" {
-		configFile = "/config/cosmos.config.json"
+		configFile = CONFIGFOLDER + "cosmos.config.json"
 	}
 
 	return configFile