소스 검색

[release] v0.10.0-unstable15

Yann Stepienik 1 년 전
부모
커밋
a6b96bc42a

+ 5 - 1
changelog.md

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

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

@@ -28,6 +28,16 @@ function restart() {
   }))
 }
 
+
+function reset() {
+  return wrap(fetch('/cosmos/api/constellation/reset', {
+    method: 'GET',
+    headers: {
+        'Content-Type': 'application/json'
+    }
+  }))
+}
+
 function getConfig() {
   return wrap(fetch('/cosmos/api/constellation/config', {
     method: 'GET',
@@ -46,10 +56,22 @@ function getLogs() {
   }))
 }
 
+function connect(file) {
+  return wrap(fetch('/cosmos/api/constellation/connect', {
+    method: 'POST',
+    headers: {
+        'Content-Type': 'application/json'
+    },
+    body: JSON.stringify(file),
+  }))
+}
+
 export {
   list,
   addDevice,
   restart,
   getConfig,
   getLogs,
+  reset,
+  connect,
 };

+ 48 - 0
client/src/components/confirmModal.jsx

@@ -0,0 +1,48 @@
+// material-ui
+import { LoadingButton } from '@mui/lab';
+import { Button } 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 { useEffect, useState } from 'react';
+
+const ConfirmModal = ({ callback, label, content }) => {
+    const [openModal, setOpenModal] = useState(false);
+
+    return <>
+      <Dialog open={openModal} onClose={() => setOpenModal(false)}>
+          <DialogTitle>Are you sure?</DialogTitle>
+          <DialogContent>
+              <DialogContentText>
+                  {content}
+              </DialogContentText>
+          </DialogContent>
+          <DialogActions>
+              <Button onClick={() => {
+                  setOpenModal(false);           
+              }}>Cancel</Button>
+              <LoadingButton
+              onClick={() => {   
+                  callback();     
+                  setOpenModal(false);    
+              }}>Confirm</LoadingButton>
+          </DialogActions>
+      </Dialog>
+
+      <Button
+          disableElevation
+          variant="outlined"
+          color="warning"
+          onClick={() => {
+              setOpenModal(true);
+          }}
+      >
+        {label}
+      </Button>
+    </>
+};
+
+export default ConfirmModal;

+ 3 - 2
client/src/components/fileUpload.jsx

@@ -2,7 +2,7 @@ import React from 'react';
 import { Button } from '@mui/material';
 import { UploadOutlined } from '@ant-design/icons';
 
-export default function UploadButtons({OnChange, accept, label}) {
+export default function UploadButtons({OnChange, accept, label, variant, fullWidth, size}) {
   return (
     <div>
       <input
@@ -14,7 +14,8 @@ export default function UploadButtons({OnChange, accept, label}) {
         onChange={OnChange}
       />
       <label htmlFor="contained-button-file">
-        <Button variant="contained" component="span" startIcon={<UploadOutlined />}>
+        <Button variant={variant || "contained"} component="span"
+        fullWidth={fullWidth} startIcon={<UploadOutlined />}>
           {label || 'Upload'}
         </Button>
       </label>

+ 120 - 31
client/src/pages/constellation/addDevice.jsx

@@ -12,10 +12,67 @@ import { PlusCircleFilled } from '@ant-design/icons';
 import { Formik } from 'formik';
 import * as yup from 'yup';
 import * as API from '../../api';
-import { CosmosFormDivider, CosmosInputText, CosmosSelect } from '../config/users/formShortcuts';
+import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../config/users/formShortcuts';
 import { DownloadFile } from '../../api/downloadButton';
 import QRCode from 'qrcode';
 
+const getDocker = (data, isCompose) => {
+  let lighthouses = '';
+
+  for (let i = 0; i < data.LighthousesList.length; i++) {
+    const l = data.LighthousesList[i];
+    lighthouses += l.publicHostname + ";" +  l.ip + ":" + l.port + ";" + l.isRelay + ",";
+  }
+
+  let containerName = "cosmos-constellation-lighthouse";
+  let imageName = "cosmos-constellation-lighthouse:latest";
+
+  let volPath = "/var/lib/cosmos-constellation";
+
+  if (isCompose) {
+    return `
+version: "3.8"
+services:
+  ${containerName}:
+    image: ${imageName}
+    container_name: ${containerName}
+    restart: unless-stopped
+    network_mode: bridge
+    ports:
+      - "${data.Port}:4242"
+    volumes:
+      - ${volPath}:/config
+    environment:
+      - CA=${JSON.stringify(data.CA)}
+      - CERT=${JSON.stringify(data.PrivateKey)}
+      - KEY=${JSON.stringify(data.PublicKey)}
+      - LIGHTHOUSES=${lighthouses}
+      - PUBLIC_HOSTNAME=${data.PublicHostname}
+      - IS_RELAY=${data.IsRelay}
+      - IP=${data.IP}
+`;
+  } else {
+    return `
+docker run -d \\
+  --name ${containerName} \\
+  --restart unless-stopped \\
+  --network bridge \\
+  -v ${volPath}:/config \\
+  -e CA=${JSON.stringify(data.CA)} \\
+  -e CERT=${JSON.stringify(data.PrivateKey)} \\
+  -e KEY=${JSON.stringify(data.PublicKey)} \\
+  -e LIGHTHOUSES=${lighthouses} \\
+  -e PUBLIC_HOSTNAME=${data.PublicHostname} \\
+  -e IS_RELAY=${data.IsRelay} \\
+  -e IP=${data.IP} \\
+  -p ${data.Port}:4242 \\
+  ${imageName}
+`;
+  }
+
+}
+
+
 const AddDeviceModal = ({ users, config, isAdmin, refreshConfig, devices }) => {
   const [openModal, setOpenModal] = useState(false);
   const [isDone, setIsDone] = useState(null);
@@ -63,12 +120,18 @@ const AddDeviceModal = ({ users, config, isAdmin, refreshConfig, devices }) => {
           deviceName: '',
           ip: firstIP,
           publicKey: '',
+          Port: "4242",
+          PublicHostname: '',
+          IsRelay: true,
+          isLighthouse: false,
         }}
 
         validationSchema={yup.object({
         })}
 
         onSubmit={(values, { setSubmitting, setStatus, setErrors }) => {
+          if(values.isLighthouse) values.nickname = null;
+
           return API.constellation.addDevice(values).then(({data}) => {
             setIsDone(data);
             refreshConfig();
@@ -85,52 +148,55 @@ const AddDeviceModal = ({ users, config, isAdmin, refreshConfig, devices }) => {
             {isDone ? <DialogContent>
               <DialogContentText>
                 <p>
-                  Device added successfully!
+                Device added successfully!
                   Download scan the QR Code from the Cosmos app or download the relevant
                   files to your device along side the config and network certificate to
                   connect:
                 </p>
 
                 <Stack spacing={2} direction={"column"}>
-                <CosmosFormDivider title={"QR Code"} />
-                <div style={{textAlign: 'center'}}>
-                <canvas style={{borderRadius: '15px'}} ref={canvasRef} />
-                </div>
-                {/* <CosmosFormDivider title={"Cosmos Client (File)"} />
-                  <DownloadFile 
-                    filename={isDone.DeviceName + `.constellation`}
-                    content={JSON.stringify(isDone, null, 2)}
-                    label={"Download " + isDone.DeviceName + `.constellation`}
-                  /> */}
+                {/* {isDone.isLighthouse ? <>
+                  <CosmosFormDivider title={"Docker"} />
+                  <TextField
+                    fullWidth
+                    multiline
+                    value={getDocker(isDone, false)}
+                    variant="outlined"
+                    size="small"
+                    disabled
+                  />
+                  <CosmosFormDivider title={"File (Docker-Compose)"} />
+                  <DownloadFile
+                    filename={`docker-compose.yml`}
+                    content={getDocker(isDone, true)}
+                    label={"Download docker-compose.yml"}
+                  />
+                </> : <> */}
+                  <CosmosFormDivider title={"QR Code"} />
+                  <div style={{textAlign: 'center'}}>
+                  <canvas style={{borderRadius: '15px'}} ref={canvasRef} />
+                  </div>
+                {/* </>} */}
+                
                 <CosmosFormDivider title={"File"} />
-
                   <DownloadFile 
                     filename={`constellation.yml`}
                     content={isDone.Config}
                     label={"Download constellation.yml"}
                   />
-                  {/* <DownloadFile
-                    filename={isDone.DeviceName + `.key`}
-                    content={isDone.PublicKey}
-                    label={"Download " + isDone.DeviceName + `.key`}
-                  />
-                  <DownloadFile
-                    filename={isDone.DeviceName + `.crt`}
-                    content={isDone.PrivateKey}
-                    label={"Download " + isDone.DeviceName + `.crt`}
-                  />
-                  <DownloadFile
-                    filename={`ca.crt`}
-                    content={isDone.CA}
-                    label={"Download ca.crt"}
-                  /> */}
                 </Stack>
               </DialogContentText>
             </DialogContent> : <DialogContent>
               <DialogContentText>
-                <p>Add a device to the constellation using either the Cosmos or Nebula client</p>
+                <p>Add a Device to the constellation using either the Cosmos or Nebula client</p>
                 <div>
                   <Stack spacing={2} style={{}}>
+                  <CosmosCheckbox
+                    name="isLighthouse"
+                    label="Lighthouse"
+                    formik={formik}
+                  />
+                  {!formik.values.isLighthouse &&
                     <CosmosSelect
                       name="nickname"
                       label="Owner"
@@ -141,7 +207,7 @@ const AddDeviceModal = ({ users, config, isAdmin, refreshConfig, devices }) => {
                           return [u.nickname, u.nickname]
                         })
                       }
-                    />
+                    />}
 
                     <CosmosInputText
                       name="deviceName"
@@ -155,12 +221,33 @@ const AddDeviceModal = ({ users, config, isAdmin, refreshConfig, devices }) => {
                       formik={formik}
                     />
 
+                    {/* <CosmosInputText
+                      name="Port"
+                      label="VPN Port (default: 4242)"
+                      formik={formik}
+                    /> */}
+
                     <CosmosInputText
                       multiline
                       name="publicKey"
                       label="Public Key (Optional)"
                       formik={formik}
                     />
+                    {formik.values.isLighthouse && <>
+                      <CosmosFormDivider title={"Lighthouse Setup"} />
+
+                      <CosmosInputText
+                        name="PublicHostname"
+                        label="Public Hostname"
+                        formik={formik}
+                      />
+
+                      <CosmosCheckbox
+                        name="IsRelay"
+                        label="Can Relay Traffic"
+                        formik={formik}
+                      />
+                    </>}
                     <div>
                       {formik.errors && formik.errors.length > 0 && <Stack spacing={2} direction={"column"}>
                         <Alert severity="error">{formik.errors.map((err) => {
@@ -189,7 +276,9 @@ const AddDeviceModal = ({ users, config, isAdmin, refreshConfig, devices }) => {
         setIsDone(null);
         setOpenModal(true);
       }}
-      variant="contained"
+      variant={
+        "contained"
+      }
       startIcon={<PlusCircleFilled />}
     >
       Add Device

+ 65 - 11
client/src/pages/constellation/index.jsx

@@ -4,14 +4,26 @@ 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 { CloudOutlined, CloudServerOutlined, CompassOutlined, DesktopOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons";
 import IsLoggedIn from "../../isLoggedIn";
-import { Button, CircularProgress, Stack } from "@mui/material";
-import { CosmosCheckbox, CosmosFormDivider } from "../config/users/formShortcuts";
+import { Alert, Button, CircularProgress, Stack } from "@mui/material";
+import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts";
 import MainCard from "../../components/MainCard";
 import { Formik } from "formik";
 import { LoadingButton } from "@mui/lab";
 import ApiModal from "../../components/apiModal";
+import { isDomain } from "../../utils/indexs";
+import ConfirmModal from "../../components/confirmModal";
+import UploadButtons from "../../components/fileUpload";
+
+const getDefaultConstellationHostname = (config) => {
+  // if domain is set, use it
+  if(isDomain(config.HTTPConfig.Hostname)) {
+    return "vpn." + config.HTTPConfig.Hostname;
+  } else {
+    return config.HTTPConfig.Hostname;
+  }
+}
 
 export const ConstellationIndex = () => {
   const [isAdmin, setIsAdmin] = useState(false);
@@ -41,6 +53,8 @@ export const ConstellationIndex = () => {
       return <DesktopOutlined />
     } else if (r.deviceName.toLowerCase().includes("tablet")) {
       return <TabletOutlined />
+    } else if (r.deviceName.toLowerCase().includes("lighthouse") || r.deviceName.toLowerCase().includes("server")) {
+      return <CompassOutlined />
     } else {
       return <CloudOutlined />
     }
@@ -53,22 +67,30 @@ export const ConstellationIndex = () => {
       <div>
         <MainCard title={"Constellation Setup"} content={config.constellationIP}>
           <Stack spacing={2}>
+          {config.ConstellationConfig.Enabled && config.ConstellationConfig.SlaveMode && <>
+            <Alert severity="info">
+              You are currently connected to an external constellation network. Use your main Cosmos server to manage your constellation network and devices.
+            </Alert>
+          </>}  
           <Formik
             initialValues={{
               Enabled: config.ConstellationConfig.Enabled,
               IsRelay: config.ConstellationConfig.NebulaConfig.Relay.AMRelay,
+              ConstellationHostname: (config.ConstellationConfig.ConstellationHostname && config.ConstellationConfig.ConstellationHostname != "") ? config.ConstellationConfig.ConstellationHostname :
+                getDefaultConstellationHostname(config)
             }}
             onSubmit={(values) => {
               let newConfig = { ...config };
               newConfig.ConstellationConfig.Enabled = values.Enabled;
               newConfig.ConstellationConfig.NebulaConfig.Relay.AMRelay = values.IsRelay;
+              newConfig.ConstellationConfig.ConstellationHostname = values.ConstellationHostname;
               return API.config.set(newConfig);
             }}
           >
             {(formik) => (
               <form onSubmit={formik.handleSubmit}>
                 <Stack spacing={2}>        
-                <Stack spacing={2} direction="row">          
+                {formik.values.Enabled && <Stack spacing={2} direction="row">    
                   <Button
                       disableElevation
                       variant="outlined"
@@ -77,14 +99,40 @@ export const ConstellationIndex = () => {
                         await API.constellation.restart();
                       }}
                     >
-                      Restart Nebula
+                      Restart VPN Service
                   </Button>
-                  <ApiModal callback={API.constellation.getLogs} label={"Show Nebula logs"} />
-                  <ApiModal callback={API.constellation.getConfig} label={"Render Nebula Config"} />
-                  </Stack>
+                  <ApiModal callback={API.constellation.getLogs} label={"Show VPN logs"} />
+                  <ApiModal callback={API.constellation.getConfig} label={"Show VPN Config"} />
+                  <ConfirmModal
+                    variant="outlined"
+                    color="warning"
+                    label={"Reset Network"}
+                    content={"This will completely reset the network, and disconnect all the clients. You will need to reconnect them. This cannot be undone."}
+                    callback={async () => {
+                      await API.constellation.reset();
+                      refreshConfig();
+                    }}
+                  />
+                  </Stack>}
                   <CosmosCheckbox formik={formik} name="Enabled" label="Constellation Enabled" />
-                  <CosmosCheckbox formik={formik} name="IsRelay" label="Relay requests via this Node" />
-
+                  {config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <>
+                    {formik.values.Enabled && <>
+                      <CosmosCheckbox formik={formik} name="IsRelay" label="Relay requests via this Node" />
+                      <Alert severity="info">This is your Constellation hostname, that you will use to connect. If you are using a domain name, this needs to be different from your server's hostname. Whatever the domain you choose, it is very important that you make sure there is a A entry in your domain DNS pointing to this server. <strong>If you change this value, you will need to reset your network and reconnect all the clients!</strong></Alert>
+                      <CosmosInputText formik={formik} name="ConstellationHostname" label="Constellation Hostname" />
+                    </>}
+                  </>}
+                  <UploadButtons
+                    accept=".yml,.yaml"
+                    label={"Upload Nebula Config"}
+                    variant="outlined"
+                    fullWidth
+                    OnChange={async (e) => {
+                      let file = e.target.files[0];
+                      await API.constellation.connect(file);
+                      refreshConfig();
+                    }}
+                  />
                   <LoadingButton
                       disableElevation
                       loading={formik.isSubmitting}
@@ -101,12 +149,13 @@ export const ConstellationIndex = () => {
           </Stack>
         </MainCard>
       </div>
+      {config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <>
       <CosmosFormDivider title={"Devices"} />
       <PrettyTableView 
           data={devices}
           getKey={(r) => r.deviceName}
           buttons={[
-            <AddDeviceModal isAdmin={isAdmin} users={users} config={config} refreshConfig={refreshConfig} devices={devices} />
+            <AddDeviceModal isAdmin={isAdmin} users={users} config={config} refreshConfig={refreshConfig} devices={devices} />,
           ]}
           columns={[
               {
@@ -121,6 +170,10 @@ export const ConstellationIndex = () => {
                   title: 'Owner',
                   field: (r) => <strong>{r.nickname}</strong>,
               },
+              {
+                  title: 'Type',
+                  field: (r) => <strong>{r.isLighthouse ? "Lighthouse" : "Client"}</strong>,
+              },
               {
                   title: 'Constellation IP',
                   screenMin: 'md', 
@@ -137,6 +190,7 @@ export const ConstellationIndex = () => {
               }
           ]}
         />
+      </>}
         </Stack>
     </> : <center>
       <CircularProgress color="inherit" size={20} />

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "cosmos-server",
-  "version": "0.10.0-unstable14",
+  "version": "0.10.0-unstable15",
   "description": "",
   "main": "test-server.js",
   "bugs": {

+ 41 - 2
src/constellation/api_devices_create.go

@@ -9,10 +9,18 @@ import (
 )
 
 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`
+	
+	// for devices only
+	Nickname string `json:"nickname",validate:"max=32,alphanum",omitempty`
+	
+	// for lighthouse only
+	IsLighthouse bool `json:"isLighthouse",omitempty`
+	IsRelay bool `json:"isRelay",omitempty`
+	PublicHostname string `json:"PublicHostname",omitempty`
+	Port string `json:"port",omitempty`
 }
 
 func DeviceCreate(w http.ResponseWriter, req *http.Request) {
@@ -67,11 +75,22 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
 				return
 			}
 
+			if request.IsLighthouse && request.Nickname != "" {
+				utils.Error("DeviceCreation: Lighthouse cannot belong to a user", nil)
+				utils.HTTPError(w, "Device Creation Error: Lighthouse cannot have a nickname",
+					http.StatusInternalServerError, "DC003")
+				return
+			}
+
 			_, err3 := c.InsertOne(nil, map[string]interface{}{
 				"Nickname": nickname,
 				"DeviceName": deviceName,
 				"PublicKey": key,
 				"IP": request.IP,
+				"IsLighthouse": request.IsLighthouse,
+				"IsRelay": request.IsRelay,
+				"PublicHostname": request.PublicHostname,
+				"Port": request.Port,
 			})
 
 			if err3 != nil {
@@ -88,9 +107,24 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
 					http.StatusInternalServerError, "DC006")
 				return
 			}
+
+			lightHousesList := []utils.ConstellationDevice{}
+			if request.IsLighthouse {
+				lightHousesList, err = GetAllLightHouses()
+			}
 			
 			// read configYml from config/nebula.yml
-			configYml, err := getYAMLClientConfig(deviceName, utils.CONFIGFOLDER + "nebula.yml", capki, cert, key)
+			configYml, err := getYAMLClientConfig(deviceName, utils.CONFIGFOLDER + "nebula.yml", capki, cert, key, utils.ConstellationDevice{
+				Nickname: nickname,
+				DeviceName: deviceName,
+				PublicKey: key,
+				IP: request.IP,
+				IsLighthouse: request.IsLighthouse,
+				IsRelay: request.IsRelay,
+				PublicHostname: request.PublicHostname,
+				Port: request.Port,
+			})
+
 			if err != nil {
 				utils.Error("DeviceCreation: Error while reading config", err)
 				utils.HTTPError(w, "Device Creation Error: " + err.Error(),
@@ -108,6 +142,11 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
 					"IP": request.IP,
 					"Config": configYml,
 					"CA": capki,
+					"IsLighthouse": request.IsLighthouse,
+					"IsRelay": request.IsRelay,
+					"PublicHostname": request.PublicHostname,
+					"Port": request.Port,
+					"LighthousesList": lightHousesList,
 				},
 			})
 		} else if err2 == nil {

+ 1 - 11
src/constellation/api_devices_list.go

@@ -29,7 +29,7 @@ func DeviceList(w http.ResponseWriter, req *http.Request) {
 		return
 	}
 	
-	var devices []utils.Device
+	var devices []utils.ConstellationDevice
 	
 	// Check if user is an admin
 	if isAdmin {
@@ -47,11 +47,6 @@ func DeviceList(w http.ResponseWriter, req *http.Request) {
 			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")
@@ -68,11 +63,6 @@ func DeviceList(w http.ResponseWriter, req *http.Request) {
 			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

+ 20 - 0
src/constellation/api_nebula.go

@@ -55,6 +55,26 @@ func API_Restart(w http.ResponseWriter, req *http.Request) {
 	}
 }
 
+func API_Reset(w http.ResponseWriter, req *http.Request) {
+	if utils.AdminOnly(w, req) != nil {
+		return
+	}
+
+	if(req.Method == "GET") {
+		ResetNebula()
+
+		utils.Log("Constellation: nebula reset")
+		
+		json.NewEncoder(w).Encode(map[string]interface{}{
+			"status": "OK",
+		})
+	} else {
+		utils.Error("SettingGet: Method not allowed" + req.Method, nil)
+		utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
+		return
+	}
+}
+
 func API_GetLogs(w http.ResponseWriter, req *http.Request) {
 	if utils.AdminOnly(w, req) != nil {
 		return

+ 47 - 0
src/constellation/api_nebula_connect.go

@@ -0,0 +1,47 @@
+package constellation
+
+import (
+	"net/http"
+	"encoding/json"
+	"io/ioutil"
+
+	
+	"github.com/azukaar/cosmos-server/src/utils" 
+)
+
+func API_ConnectToExisting(w http.ResponseWriter, req *http.Request) {
+	if utils.AdminOnly(w, req) != nil {
+		return
+	}
+
+	if(req.Method == "POST") {
+		body, err := ioutil.ReadAll(req.Body)
+		if err != nil {
+			utils.Error("API_Restart: Invalid User Request", err)
+			utils.HTTPError(w, "API_Restart Error",
+				http.StatusInternalServerError, "AR001")
+			return	
+		}
+
+		config := utils.ReadConfigFromFile()
+		config.ConstellationConfig.Enabled = true
+		config.ConstellationConfig.SlaveMode = true
+		config.ConstellationConfig.DNS = false
+		// ConstellationHostname = 
+
+		// output utils.CONFIGFOLDER + "nebula.yml"
+		err = ioutil.WriteFile(utils.CONFIGFOLDER + "nebula.yml", body, 0644)
+		
+		utils.SetBaseMainConfig(config)
+		
+		RestartNebula()
+		
+		json.NewEncoder(w).Encode(map[string]interface{}{
+			"status": "OK",
+		})
+	} else {
+		utils.Error("SettingGet: Method not allowed" + req.Method, nil)
+		utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
+		return
+	}
+}

+ 24 - 20
src/constellation/index.go

@@ -6,32 +6,36 @@ import (
 )
 
 func Init() {
+	var err error
+	
 	// if Constellation is enabled
 	if utils.GetMainConfig().ConstellationConfig.Enabled {
-		InitConfig()
-		
-		utils.Log("Initializing Constellation module...")
+		if !utils.GetMainConfig().ConstellationConfig.SlaveMode {
+			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 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().ConstellationConfig.ConstellationHostname)
+			}
 
-		// 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.1/24", "", true)
-		}
+			// 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.1/24", "", true)
+			}
 
-		// export nebula.yml
-		utils.Log("Constellation: exporting nebula.yml...")
-		err := ExportConfigToYAML(utils.GetMainConfig().ConstellationConfig, utils.CONFIGFOLDER + "nebula.yml")
+			// export nebula.yml
+			utils.Log("Constellation: exporting nebula.yml...")
+			err := ExportConfigToYAML(utils.GetMainConfig().ConstellationConfig, utils.CONFIGFOLDER + "nebula.yml")
 
-		if err != nil {
-			utils.Error("Constellation: error while exporting nebula.yml", err)
+			if err != nil {
+				utils.Error("Constellation: error while exporting nebula.yml", err)
+			}
 		}
 		
 		// start nebula

+ 120 - 8
src/constellation/nebula.go

@@ -80,18 +80,92 @@ func RestartNebula() {
 	Init()
 }
 
+func ResetNebula() error {
+	stop()
+	utils.Log("Resetting nebula...")
+	os.RemoveAll(utils.CONFIGFOLDER + "nebula.yml")
+	os.RemoveAll(utils.CONFIGFOLDER + "ca.crt")
+	os.RemoveAll(utils.CONFIGFOLDER + "ca.key")
+	os.RemoveAll(utils.CONFIGFOLDER + "cosmos.crt")
+	os.RemoveAll(utils.CONFIGFOLDER + "cosmos.key")
+	// remove everything in db
+
+	c, err := utils.GetCollection(utils.GetRootAppId(), "devices")
+	if err != nil {
+			return err
+	}
+
+	_, err = c.DeleteMany(nil, map[string]interface{}{})
+	if err != nil {
+		return err
+	}
+
+	Init()
+
+	return nil
+}
+
+func GetAllLightHouses() ([]utils.ConstellationDevice, error) {
+	c, err := utils.GetCollection(utils.GetRootAppId(), "devices")
+	if err != nil {
+		return []utils.ConstellationDevice{}, err
+	}
+
+	var devices []utils.ConstellationDevice
+
+	cursor, err := c.Find(nil, map[string]interface{}{
+		"IsLighthouse": true,
+	})
+	cursor.All(nil, &devices)
+
+	if err != nil {
+		return []utils.ConstellationDevice{}, err
+	}
+
+	return devices, nil
+}
+
+func cleanIp(ip string) string {
+	return strings.Split(ip, "/")[0]
+}
+
 func ExportConfigToYAML(overwriteConfig utils.ConstellationConfig, outputPath string) error {
 	// Combine defaultConfig and overwriteConfig
 	finalConfig := NebulaDefaultConfig
 
 	finalConfig.StaticHostMap = map[string][]string{
 		"192.168.201.1": []string{
-			utils.GetMainConfig().HTTPConfig.Hostname + ":4242",
+			utils.GetMainConfig().ConstellationConfig.ConstellationHostname + ":4242",
 		},
 	}
 
+	// for each lighthouse
+	lh, err := GetAllLightHouses()
+	if err != nil {
+		return err
+	}
+
+	for _, l := range lh {
+		finalConfig.StaticHostMap[cleanIp(l.IP)] = []string{
+			l.PublicHostname + ":" + l.Port,
+		}
+	}
+	
+	// add other lighthouses 
+	finalConfig.Lighthouse.Hosts = []string{}
+	for _, l := range lh {
+		finalConfig.Lighthouse.Hosts = append(finalConfig.Lighthouse.Hosts, cleanIp(l.IP))
+	}
+
 	finalConfig.Relay.AMRelay = overwriteConfig.NebulaConfig.Relay.AMRelay
 
+	finalConfig.Relay.Relays = []string{}
+	for _, l := range lh {
+		if l.IsRelay && l.IsLighthouse {
+			finalConfig.Relay.Relays = append(finalConfig.Relay.Relays, cleanIp(l.IP))
+		}
+	}
+
 	// Marshal the combined config to YAML
 	yamlData, err := yaml.Marshal(finalConfig)
 	if err != nil {
@@ -118,7 +192,7 @@ func ExportConfigToYAML(overwriteConfig utils.ConstellationConfig, outputPath st
 	return nil
 }
 
-func getYAMLClientConfig(name, configPath, capki, cert, key string) (string, error) {
+func getYAMLClientConfig(name, configPath, capki, cert, key string, device utils.ConstellationDevice) (string, error) {
 	utils.Log("Exporting YAML config for " + name + " with file " + configPath)
 
 	// Read the YAML config file
@@ -134,21 +208,38 @@ func getYAMLClientConfig(name, configPath, capki, cert, key string) (string, err
 		return "", err
 	}
 
+	lh, err := GetAllLightHouses()
+	if err != nil {
+		return "", err
+	}
+
 	if staticHostMap, ok := configMap["static_host_map"].(map[interface{}]interface{}); ok {
 		staticHostMap["192.168.201.1"] = []string{
-			utils.GetMainConfig().HTTPConfig.Hostname + ":4242",
+			utils.GetMainConfig().ConstellationConfig.ConstellationHostname + ":4242",
+		}
+		
+		for _, l := range lh {
+			staticHostMap[cleanIp(l.IP)] = []string{
+				l.PublicHostname + ":" + l.Port,
+			}
 		}
 	} else {
 		return "", errors.New("static_host_map not found in nebula.yml")
 	}
 
-	// set lightHouse to false
+	// set lightHouse
 	if lighthouseMap, ok := configMap["lighthouse"].(map[interface{}]interface{}); ok {
-		lighthouseMap["am_lighthouse"] = false
-
+		lighthouseMap["am_lighthouse"] = device.IsLighthouse
+		
 		lighthouseMap["hosts"] = []string{
 			"192.168.201.1",
 		}
+		
+		for _, l := range lh {
+			if cleanIp(l.IP) != cleanIp(device.IP) {
+				lighthouseMap["hosts"] = append(lighthouseMap["hosts"].([]string), cleanIp(l.IP))
+			}
+		}
 	} else {
 		return "", errors.New("lighthouse not found in nebula.yml")
 	}
@@ -162,13 +253,34 @@ func getYAMLClientConfig(name, configPath, capki, cert, key string) (string, err
 	}
 
 	if relayMap, ok := configMap["relay"].(map[interface{}]interface{}); ok {
-		relayMap["am_relay"] = false
-		relayMap["relays"] = []string{"192.168.201.1"}
+		relayMap["am_relay"] = device.IsRelay && device.IsLighthouse
+		relayMap["relays"] = []string{}
+		if utils.GetMainConfig().ConstellationConfig.NebulaConfig.Relay.AMRelay {
+			relayMap["relays"] = append(relayMap["relays"].([]string), "192.168.201.1")
+		}
+
+		for _, l := range lh {
+			if l.IsRelay && l.IsLighthouse && cleanIp(l.IP) != cleanIp(device.IP) {
+				relayMap["relays"] = append(relayMap["relays"].([]string), cleanIp(l.IP))
+			}
+		}
 	} else {
 		return "", errors.New("relay not found in nebula.yml")
 	}
+	
+	if listen, ok := configMap["listen"].(map[interface{}]interface{}); ok {
+		if device.Port != "" {
+			listen["port"] = device.Port
+		} else {
+			listen["port"] = "4242"
+		}
+	} else {
+		return "", errors.New("listen not found in nebula.yml")
+	}
 
 	configMap["deviceName"] = name
+	configMap["local_dns_overwrite"] = "192.168.201.1"
+	configMap["public_hostname"] = device.PublicHostname
 
 	// export configMap as YML
 	yamlData, err = yaml.Marshal(configMap)

+ 2 - 0
src/httpServer.go

@@ -334,6 +334,8 @@ func InitServer() *mux.Router {
 
 	srapi.HandleFunc("/api/constellation/devices", constellation.ConstellationAPIDevices)
 	srapi.HandleFunc("/api/constellation/restart", constellation.API_Restart)
+	srapi.HandleFunc("/api/constellation/reset", constellation.API_Reset)
+	srapi.HandleFunc("/api/constellation/connect", constellation.API_ConnectToExisting)
 	srapi.HandleFunc("/api/constellation/config", constellation.API_GetConfig)
 	srapi.HandleFunc("/api/constellation/logs", constellation.API_GetLogs)
 

+ 19 - 8
src/proxy/routeTo.go

@@ -46,7 +46,7 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) {
 
 
 // NewProxy takes target host and creates a reverse proxy
-func NewProxy(targetHost string, AcceptInsecureHTTPSTarget bool, VerboseForwardHeader bool, DisableHeaderHardening bool, CORSOrigin string) (*httputil.ReverseProxy, error) {
+func NewProxy(targetHost string, AcceptInsecureHTTPSTarget bool, VerboseForwardHeader bool, DisableHeaderHardening bool, CORSOrigin string, route utils.ProxyRouteConfig) (*httputil.ReverseProxy, error) {
 	url, err := url.Parse(targetHost)
 	if err != nil {
 			return nil, err
@@ -76,15 +76,28 @@ func NewProxy(targetHost string, AcceptInsecureHTTPSTarget bool, VerboseForwardH
 			req.Header.Set("X-Forwarded-Ssl", "on")
 		}
 
-		if CORSOrigin != "" {
-			req.Header.Set("X-Forwarded-Host", url.Host)
+		req.Header.Del("X-Origin-Host")
+		req.Header.Del("X-Forwarded-Host")
+		req.Header.Del("X-Forwarded-For")
+		req.Header.Del("X-Real-Ip")
+		req.Header.Del("Host")
+
+		hostname := utils.GetMainConfig().HTTPConfig.Hostname
+		if route.Host != "" && route.UseHost {
+			hostname = route.Host
+		}
+		if route.UsePathPrefix {
+			hostname = hostname + route.PathPrefix
 		}
 
 		if VerboseForwardHeader {
-			req.Header.Set("X-Origin-Host", url.Host)
-			req.Header.Set("Host", url.Host)
+			req.Header.Set("X-Origin-Host", hostname)
+			req.Header.Set("Host", hostname)
+			req.Header.Set("X-Forwarded-Host", hostname)
 			req.Header.Set("X-Forwarded-For", utils.GetClientIP(req))
 			req.Header.Set("X-Real-IP", utils.GetClientIP(req))
+		} else {
+			req.Host = url.Host
 		}
 	}
 
@@ -100,8 +113,6 @@ func NewProxy(targetHost string, AcceptInsecureHTTPSTarget bool, VerboseForwardH
 
 		if CORSOrigin != "" {
 			resp.Header.Del("Access-Control-Allow-Origin")
-			resp.Header.Del("Access-Control-Allow-Methods")
-			resp.Header.Del("Access-Control-Allow-Headers")
 			resp.Header.Del("Access-Control-Allow-Credentials")
 		}
 		
@@ -126,7 +137,7 @@ func RouteTo(route utils.ProxyRouteConfig) http.Handler {
 	routeType := route.Mode
 
 	if(routeType == "SERVAPP" || routeType == "PROXY") {
-		proxy, err := NewProxy(destination, route.AcceptInsecureHTTPSTarget, route.VerboseForwardHeader, route.DisableHeaderHardening, route.CORSOrigin)
+		proxy, err := NewProxy(destination, route.AcceptInsecureHTTPSTarget, route.VerboseForwardHeader, route.DisableHeaderHardening, route.CORSOrigin, route)
 		if err != nil {
 				utils.Error("Create Route", err)
 		}

+ 2 - 2
src/proxy/routerGen.go

@@ -30,12 +30,12 @@ func tokenMiddleware(enabled bool, adminOnly bool) func(next http.Handler) http.
 			r.Header.Set("x-cosmos-mfa", strconv.Itoa((int)(u.MFAState)))
 
 			ogcookies := r.Header.Get("Cookie")
-			cookieRemoveRegex := regexp.MustCompile(`jwttoken=[^;]*;`)
+			cookieRemoveRegex := regexp.MustCompile(`\s?jwttoken=[^;]*;?\s?`)
 			cookies := cookieRemoveRegex.ReplaceAllString(ogcookies, "")
 			r.Header.Set("Cookie", cookies)
 
 			// Replace the token with a application speicfic one
-			r.Header.Set("x-cosmos-token", "1234567890")
+			//r.Header.Set("x-cosmos-token", "1234567890")
 
 			if enabled && adminOnly {
 				if errT := utils.AdminOnlyWithRedirect(w, r); errT != nil {

+ 0 - 2
src/utils/middleware.go

@@ -82,8 +82,6 @@ func CORSHeader(origin string) func(next http.Handler) http.Handler {
 
 			if origin != "" {
 				w.Header().Set("Access-Control-Allow-Origin", origin)
-				w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
-				w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
 				w.Header().Set("Access-Control-Allow-Credentials", "true")
 			}
 

+ 13 - 0
src/utils/types.go

@@ -211,6 +211,7 @@ type MarketSource struct {
 
 type ConstellationConfig struct {
 	Enabled bool
+	SlaveMode bool
 	DNS bool
 	DNSPort string
 	DNSFallback string
@@ -218,6 +219,18 @@ type ConstellationConfig struct {
 	DNSAdditionalBlocklists []string
 	CustomDNSEntries map[string]string
 	NebulaConfig NebulaConfig
+	ConstellationHostname string
+}
+
+type ConstellationDevice struct {
+	Nickname string `json:"nickname"`
+	DeviceName string `json:"deviceName"`
+	PublicKey string `json:"publicKey"`
+	IP string `json:"ip"`
+	IsLighthouse bool `json:"isLighthouse"`
+	IsRelay bool `json:"isRelay"`
+	PublicHostname string `json:"publicHostname"`
+	Port string `json:"port"`
 }
 
 type NebulaFirewallRule struct {

+ 17 - 0
src/utils/utils.go

@@ -214,6 +214,15 @@ func LoadBaseMainConfig(config Config) {
 	if MainConfig.DockerConfig.DefaultDataPath == "" {
 		MainConfig.DockerConfig.DefaultDataPath = "/usr"
 	}
+	
+	if MainConfig.ConstellationConfig.ConstellationHostname == "" {
+		// if hostname is a domain add vpn. suffix otherwise use hostname
+		if IsDomain(MainConfig.HTTPConfig.Hostname) {
+			MainConfig.ConstellationConfig.ConstellationHostname = "vpn." + MainConfig.HTTPConfig.Hostname
+		} else {
+			MainConfig.ConstellationConfig.ConstellationHostname = MainConfig.HTTPConfig.Hostname
+		}
+	}
 }
 
 func GetMainConfig() Config {
@@ -577,4 +586,12 @@ func GetClientIP(req *http.Request) string {
 		ip = req.RemoteAddr
 	}*/
 	return req.RemoteAddr
+}
+
+func IsDomain(domain string) bool {
+	// contains . and at least a letter and no special characters invalid in a domain
+	if strings.Contains(domain, ".") && strings.ContainsAny(domain, "abcdefghijklmnopqrstuvwxyz") && !strings.ContainsAny(domain, " !@#$%^&*()+=[]{}\\|;:'\",/<>?") {
+		return true
+	}
+	return false
 }