diff --git a/changelog.md b/changelog.md index d3d2ed4..1f51c44 100644 --- a/changelog.md +++ b/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 diff --git a/client/src/api/constellation.tsx b/client/src/api/constellation.tsx index 61b0923..375aabe 100644 --- a/client/src/api/constellation.tsx +++ b/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, }; \ No newline at end of file diff --git a/client/src/components/confirmModal.jsx b/client/src/components/confirmModal.jsx new file mode 100644 index 0000000..81bf673 --- /dev/null +++ b/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 <> + setOpenModal(false)}> + Are you sure? + + + {content} + + + + + { + callback(); + setOpenModal(false); + }}>Confirm + + + + + +}; + +export default ConfirmModal; diff --git a/client/src/components/fileUpload.jsx b/client/src/components/fileUpload.jsx index a7ba9fc..a541984 100644 --- a/client/src/components/fileUpload.jsx +++ b/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 (
diff --git a/client/src/pages/constellation/addDevice.jsx b/client/src/pages/constellation/addDevice.jsx index 7d401ec..ea2aea8 100644 --- a/client/src/pages/constellation/addDevice.jsx +++ b/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 ?

- 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:

- -
- -
- {/* - */} + {/* {isDone.isLighthouse ? <> + + + + + : <> */} + +
+ +
+ {/* } */} + - - {/* - - */}
: -

Add a device to the constellation using either the Cosmos or Nebula client

+

Add a Device to the constellation using either the Cosmos or Nebula client

+ + {!formik.values.isLighthouse && { return [u.nickname, u.nickname] }) } - /> + />} { formik={formik} /> + {/* */} + + {formik.values.isLighthouse && <> + + + + + + }
{formik.errors && formik.errors.length > 0 && {formik.errors.map((err) => { @@ -189,7 +276,9 @@ const AddDeviceModal = ({ users, config, isAdmin, refreshConfig, devices }) => { setIsDone(null); setOpenModal(true); }} - variant="contained" + variant={ + "contained" + } startIcon={} > Add Device diff --git a/client/src/pages/constellation/index.jsx b/client/src/pages/constellation/index.jsx index f11d2d3..d4d0518 100644 --- a/client/src/pages/constellation/index.jsx +++ b/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 } else if (r.deviceName.toLowerCase().includes("tablet")) { return + } else if (r.deviceName.toLowerCase().includes("lighthouse") || r.deviceName.toLowerCase().includes("server")) { + return } else { return } @@ -53,22 +67,30 @@ export const ConstellationIndex = () => {
+ {config.ConstellationConfig.Enabled && config.ConstellationConfig.SlaveMode && <> + + You are currently connected to an external constellation network. Use your main Cosmos server to manage your constellation network and devices. + + } { 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) => (
- + {formik.values.Enabled && - - - + + + { + await API.constellation.reset(); + refreshConfig(); + }} + /> + } - - + {config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <> + {formik.values.Enabled && <> + + 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. If you change this value, you will need to reset your network and reconnect all the clients! + + } + } + { + let file = e.target.files[0]; + await API.constellation.connect(file); + refreshConfig(); + }} + /> {
+ {config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <> r.deviceName} buttons={[ - + , ]} columns={[ { @@ -121,6 +170,10 @@ export const ConstellationIndex = () => { title: 'Owner', field: (r) => {r.nickname}, }, + { + title: 'Type', + field: (r) => {r.isLighthouse ? "Lighthouse" : "Client"}, + }, { title: 'Constellation IP', screenMin: 'md', @@ -137,6 +190,7 @@ export const ConstellationIndex = () => { } ]} /> + }
:
diff --git a/package.json b/package.json index fd46e32..f5bc0e2 100644 --- a/package.json +++ b/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": { diff --git a/src/constellation/api_devices_create.go b/src/constellation/api_devices_create.go index f37e54a..f7ea61d 100644 --- a/src/constellation/api_devices_create.go +++ b/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 { diff --git a/src/constellation/api_devices_list.go b/src/constellation/api_devices_list.go index c2efc0f..df8b151 100644 --- a/src/constellation/api_devices_list.go +++ b/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 diff --git a/src/constellation/api_nebula.go b/src/constellation/api_nebula.go index 38be4a5..1d67c2f 100644 --- a/src/constellation/api_nebula.go +++ b/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 diff --git a/src/constellation/api_nebula_connect.go b/src/constellation/api_nebula_connect.go new file mode 100644 index 0000000..8972c74 --- /dev/null +++ b/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 + } +} diff --git a/src/constellation/index.go b/src/constellation/index.go index 0697166..6f6bbba 100644 --- a/src/constellation/index.go +++ b/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 diff --git a/src/constellation/nebula.go b/src/constellation/nebula.go index da8d614..7645e57 100644 --- a/src/constellation/nebula.go +++ b/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) diff --git a/src/httpServer.go b/src/httpServer.go index 76c2fc9..845fd66 100644 --- a/src/httpServer.go +++ b/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) diff --git a/src/proxy/routeTo.go b/src/proxy/routeTo.go index e139507..cf98a5f 100644 --- a/src/proxy/routeTo.go +++ b/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) } diff --git a/src/proxy/routerGen.go b/src/proxy/routerGen.go index f4aef7c..8dd32ae 100644 --- a/src/proxy/routerGen.go +++ b/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 { diff --git a/src/utils/middleware.go b/src/utils/middleware.go index e06bf40..b2ff755 100644 --- a/src/utils/middleware.go +++ b/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") } diff --git a/src/utils/types.go b/src/utils/types.go index 8310cba..8d4460a 100644 --- a/src/utils/types.go +++ b/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 { diff --git a/src/utils/utils.go b/src/utils/utils.go index f131fa2..1a32bed 100644 --- a/src/utils/utils.go +++ b/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 } \ No newline at end of file