Skip to content

Commit

Permalink
V0.3.16 #patch (#350)
Browse files Browse the repository at this point in the history
v0.3.16
Fixes
- Security fix for OTP bypass on plugin update
- Clean up Plugin URI route
- Fix PFW Abort
- Clean up OTP handling

---------

Co-authored-by: lts-po <[email protected]>
  • Loading branch information
lts-rad and lts-po authored Aug 13, 2024
1 parent 00189a0 commit 647b5af
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 47 deletions.
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

0 comments on commit 647b5af

Please sign in to comment.