Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V0.3.16 #patch #350

Merged
merged 11 commits into from
Aug 13, 2024
8 changes: 7 additions & 1 deletion RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
# Secure Programmable Router (SPR) Release Notes

## v0.3.15
## v0.3.16
Fixes
* Security fix for OTP bypass on plugin update
* Clean up Plugin URI route
* Fix PFW Abort
* Clean up OTP handling

## v0.3.15
Fixes
* Address APNS memory consumption bug
* Ping API call was broken
Expand Down
80 changes: 70 additions & 10 deletions api/code/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"slices"
"strings"
"time"
)
Expand Down Expand Up @@ -44,6 +45,20 @@ type PluginConfig struct {
ScopedPaths []string
}

func (p PluginConfig) MatchesData(q PluginConfig) bool {
//compare all but Enabled.
return p.Name == q.Name &&
p.URI == q.URI &&
p.UnixPath == q.UnixPath &&
p.Plus == q.Plus &&
p.GitURL == q.GitURL &&
p.ComposeFilePath == q.ComposeFilePath &&
p.HasUI == q.HasUI &&
p.SandboxedUI == q.SandboxedUI &&
p.InstallTokenPath == q.InstallTokenPath &&
slices.Compare(p.ScopedPaths, q.ScopedPaths) == 0
}

var gPlusExtensionDefaults = []PluginConfig{
{"PFW", "pfw", "/state/plugins/pfw/socket", false, true, PfwGitURL, "plugins/plus/pfw_extension/docker-compose.yml", false, false, "", []string{}},
{"MESH", "mesh", MeshdSocketPath, false, true, MeshGitURL, "plugins/plus/mesh_extension/docker-compose.yml", false, false, "", []string{}},
Expand Down Expand Up @@ -302,10 +317,13 @@ func updatePlugins(router *mux.Router, router_public *mux.Router) func(http.Resp
found := false
idx := -1
oldComposeFilePath := plugin.ComposeFilePath
currentPlugin := PluginConfig{}

for idx_, entry := range config.Plugins {
idx = idx_
if entry.Name == name || entry.Name == plugin.Name {
found = true
currentPlugin = entry
oldComposeFilePath = entry.ComposeFilePath
break
}
Expand All @@ -319,16 +337,28 @@ func updatePlugins(router *mux.Router, router_public *mux.Router) func(http.Resp

//if a GitURL is set, ensure OTP authentication for 'admin'
if !plugin.Plus && plugin.GitURL != "" {
if hasValidJwtOtpHeader("admin", r) {
http.Error(w, "OTP Token invalid for Remote Install", 400)

check_otp := true
if found {
if currentPlugin.MatchesData(plugin) {
//for on/off with Enabled state don't need to validate the otp
check_otp = false
}
}

if check_otp && !hasValidJwtOtpHeader("admin", r) {
http.Redirect(w, r, "/auth/validate", 302)
return
}

//clone but don't auto-config.
ret := downloadUserExtension(plugin.GitURL, false)
if ret == false {
fmt.Println("Failed to download extension " + plugin.GitURL)
// fall thru, dont fail
//download new plugins
if !found {
//clone but don't auto-config.
ret := downloadUserExtension(plugin.GitURL, false)
if ret == false {
fmt.Println("Failed to download extension " + plugin.GitURL)
// fall thru, dont fail
}
}
}

Expand Down Expand Up @@ -693,6 +723,20 @@ func startExtension(composeFilePath string) bool {
return true
}

func restartExtension(composeFilePath string) bool {
if composeFilePath == "" {
//no-op
return true
}

_, err := superdRequest("restart", url.Values{"compose_file": {composeFilePath}}, nil)
if err != nil {
return false
}

return true
}

func updateExtension(composeFilePath string) bool {
_, err := superdRequest("update", url.Values{"compose_file": {composeFilePath}}, nil)
if err != nil {
Expand Down Expand Up @@ -831,11 +875,27 @@ func startExtensionServices() error {
if !updateExtension(entry.ComposeFilePath) {
return errors.New("Could not update Extension at " + entry.ComposeFilePath)
}
}

if !startExtension(entry.ComposeFilePath) {
return errors.New("Could not start Extension at " + entry.ComposeFilePath)
//if it is pfw we restart for fw rules to refresh after api
if entry.Name == "PFW" {
if !restartExtension(entry.ComposeFilePath) {
//try a start
if !startExtension(entry.ComposeFilePath) {
return errors.New("Could not start Extension at " + entry.ComposeFilePath)
}
}
} else {
if !startExtension(entry.ComposeFilePath) {
return errors.New("Could not start Extension at " + entry.ComposeFilePath)
}
}

} else {
if !startExtension(entry.ComposeFilePath) {
return errors.New("Could not start Extension at " + entry.ComposeFilePath)
}
}

}
}
return nil
Expand Down
29 changes: 29 additions & 0 deletions frontend/src/components/Auth/OTPValidate.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { useNavigate } from 'react-router-dom'

import { authAPI, setJWTOTPHeader } from 'api'

import {
Expand All @@ -12,11 +14,14 @@ import {
FormControlErrorText,
Input,
InputField,
Text,
VStack
} from '@gluestack-ui/themed'

const OTPValidate = ({ onSuccess, ...props }) => {
const navigate = useNavigate()
const [code, setCode] = useState('')
const [status, setStatus] = useState('')
const [errors, setErrors] = useState({})

const otp = (e) => {
Expand All @@ -38,11 +43,35 @@ const OTPValidate = ({ onSuccess, ...props }) => {
}

useEffect(() => {
authAPI
.statusOTP()
.then((res) => {
setStatus(res.State)
})
.catch((err) => {})

if (!code.length) {
setErrors({})
}
}, [code])

if (status == 'unregistered') {
return (
<VStack space="md">
<Text>Need to setup OTP auth for this feature</Text>
<Button
variant="outline"
onPress={() => {
navigate('/admin/auth')
onSuccess() // only to close the modal
}}
>
<ButtonText>Setup OTP</ButtonText>
</Button>
</VStack>
)
}

return (
<VStack space="md">
<FormControl isInvalid={'validate' in errors}>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Dashboard/WifiWidgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ const WifiWidget = ({
color="$muted800"
sx={{ _dark: { color: '$muted400' } }}
>
Setup Complete

</Text>
</VStack>
)}
Expand Down
24 changes: 13 additions & 11 deletions frontend/src/components/Flow/FlowCards.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,16 @@ const triggers = [
}
]

const niceDockerName = (c) => {
return (c.Names[0] || c.Id.substr(0, 8)).replace(/^\//, '')
}

const niceDockerLabel = (c) => {
let name = niceDockerName(c)
let ports = c.Ports.filter((p) => p.IP != '::').map((p) => p.PrivatePort) // p.Type
return `${name}:${ports}`
}

//NOTE: titles have to match FlowList.js
// or they may become invisible.
const actions = [
Expand Down Expand Up @@ -664,14 +674,6 @@ const actions = [
DstPort: '8080',
Dst: { IP: '1.2.3.4' }
},
niceDockerName: function (c) {
return (c.Names[0] || c.Id.substr(0, 8)).replace(/^\//, '')
},
niceDockerLabel: function (c) {
let name = this.niceDockerName(c)
let ports = c.Ports.filter((p) => p.IP != '::').map((p) => p.PrivatePort) // p.Type
return `${name}:${ports}`
},
getOptions: async function (name = 'DstPort') {
if (name == 'Protocol') {
return labelsProtocol
Expand All @@ -690,8 +692,8 @@ const actions = [
.map((c) => {
return {
icon: ContainerIcon,
label: this.niceDockerLabel(c),
value: this.niceDockerName(c)
label: niceDockerLabel(c),
value: niceDockerName(c)
}
})

Expand All @@ -703,7 +705,7 @@ const actions = [
preSubmit: async function () {
let containers = await api.get('/info/docker')
let container = containers.find(
(c) => this.niceDockerName(c) == this.values.Container
(c) => niceDockerName(c) == this.values.Container
)

if (!container) {
Expand Down
18 changes: 8 additions & 10 deletions frontend/src/components/Plugins/InstallPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,16 @@ const InstallPlugin = ({ ...props }) => {
})
.catch((err) => {
if (err.response) {
err.response
.text()
.then((data) => {
if (data.includes('Invalid JWT')) {
context.error(`One Time Passcode Authentication Required, failure: ${data}`)
} else {
context.error(`Check Plugin URL: ${data}`)
}
err.response.text().then((data) => {
if (data.includes('Invalid JWT')) {
//context.error(`One Time Passcode Authentication Required, failure: ${data}`)
//NOTE this is catched outside of here and will show the OTP modal
} else {
context.error(`Check Plugin URL: ${data}`)
}
)
})
} else {
context.error(`API Error`, err)
context.error(`API Error`, err)
}
})

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Plugins/PluginList.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const PluginListItem = ({ item, deleteListItem, handleChange, ...props }) => {
size="sm"
onPress={() =>
navigate(
'/admin/custom_plugin/' + encodeURIComponent(item.URI)
`/admin/custom_plugin/${encodeURIComponent(item.URI)}/`
)
}
>
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/layouts/Admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,11 +360,14 @@ const AdminLayout = ({ toggleColorMode, ...props }) => {
.then((res) => {
setFeatures([...res])
setIsWifiDisabled(!res.includes('wifi'))

meshAPI
.leafMode()
.then((res) => setIsMeshNode(JSON.parse(res) === true))
.catch((err) => {})
.then((res) => {
setIsMeshNode(JSON.parse(res) === true)
})
.catch((err) => {
console.log(err)
})
})
.catch((err) => {
setIsWifiDisabled(true)
Expand Down
22 changes: 12 additions & 10 deletions frontend/src/views/Mesh.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useContext, useEffect, useRef, useState } from 'react'
import { AlertContext } from 'AppContext'
import { copy } from 'utils'
import { useNavigate } from 'react-router-dom'

import {
Button,
Expand All @@ -19,18 +20,18 @@ import {
CopyIcon
} from '@gluestack-ui/themed'

import { RefreshCwIcon } from 'lucide-react-native'

import api, { wifiAPI, meshAPI, authAPI } from 'api'
import APIWifi from 'api/Wifi'

import APIMesh from 'api/mesh'

import ModalForm from 'components/ModalForm'
import AddLeafRouter from 'components/Mesh/AddLeafRouter'
import { ListHeader, ListItem } from 'components/List'
import TokenItem from 'components/TokenItem'


import { RefreshCwIcon } from 'lucide-react-native'

import api, { wifiAPI, meshAPI, authAPI, setAuthReturn } from 'api'
import APIWifi from 'api/Wifi'
import APIMesh from 'api/mesh'

const Mesh = (props) => {
const [leafRouters, setLeafRouters] = useState([])
const [isLeafMode, setIsLeafMode] = useState([])
Expand All @@ -39,10 +40,11 @@ const Mesh = (props) => {
const [ssid, setSsid] = useState('')

const [mesh, setMesh] = useState({})
let [meshAvailable, setMeshAvailable] = useState(true)

let alertContext = useContext(AlertContext)
let refModal = useRef(null)
let [meshAvailable, setMeshAvailable] = useState(true)
const navigate = useNavigate()

const catchMeshErr = (err) => {
if (err?.message == 404 || err?.message == 502) {
Expand Down Expand Up @@ -283,8 +285,8 @@ const Mesh = (props) => {
})
.catch((e) => {
alertContext.error('Could not list API Tokens. Verify OTP on Auth page')
//setAuthReturn('/admin/mesh')
//navigate('/auth/validate')
setAuthReturn('/admin/auth')
navigate('/auth/validate')
})
}

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/views/Wireguard.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ const Wireguard = (props) => {
}

const updateNewDomain = () => {
if (!pendingEndpoint.length) {
return context.error('Missing endpoint domain')
}

let s = endpoints ? endpoints.concat(pendingEndpoint) : [pendingEndpoint]
setPendingEndpoint('')
setShowInput(false)
Expand Down
Loading
Loading