Skip to content

Commit 647b5af

Browse files
lts-radlts-po
and
lts-po
authored
V0.3.16 #patch (#350)
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]>
1 parent 00189a0 commit 647b5af

File tree

11 files changed

+162
-47
lines changed

11 files changed

+162
-47
lines changed

RELEASE-NOTES.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
# Secure Programmable Router (SPR) Release Notes
22

3-
## v0.3.15
3+
## v0.3.16
4+
Fixes
5+
* Security fix for OTP bypass on plugin update
6+
* Clean up Plugin URI route
7+
* Fix PFW Abort
8+
* Clean up OTP handling
49

10+
## v0.3.15
511
Fixes
612
* Address APNS memory consumption bug
713
* Ping API call was broken

api/code/plugins.go

+70-10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"os/exec"
1616
"path/filepath"
1717
"regexp"
18+
"slices"
1819
"strings"
1920
"time"
2021
)
@@ -44,6 +45,20 @@ type PluginConfig struct {
4445
ScopedPaths []string
4546
}
4647

48+
func (p PluginConfig) MatchesData(q PluginConfig) bool {
49+
//compare all but Enabled.
50+
return p.Name == q.Name &&
51+
p.URI == q.URI &&
52+
p.UnixPath == q.UnixPath &&
53+
p.Plus == q.Plus &&
54+
p.GitURL == q.GitURL &&
55+
p.ComposeFilePath == q.ComposeFilePath &&
56+
p.HasUI == q.HasUI &&
57+
p.SandboxedUI == q.SandboxedUI &&
58+
p.InstallTokenPath == q.InstallTokenPath &&
59+
slices.Compare(p.ScopedPaths, q.ScopedPaths) == 0
60+
}
61+
4762
var gPlusExtensionDefaults = []PluginConfig{
4863
{"PFW", "pfw", "/state/plugins/pfw/socket", false, true, PfwGitURL, "plugins/plus/pfw_extension/docker-compose.yml", false, false, "", []string{}},
4964
{"MESH", "mesh", MeshdSocketPath, false, true, MeshGitURL, "plugins/plus/mesh_extension/docker-compose.yml", false, false, "", []string{}},
@@ -302,10 +317,13 @@ func updatePlugins(router *mux.Router, router_public *mux.Router) func(http.Resp
302317
found := false
303318
idx := -1
304319
oldComposeFilePath := plugin.ComposeFilePath
320+
currentPlugin := PluginConfig{}
321+
305322
for idx_, entry := range config.Plugins {
306323
idx = idx_
307324
if entry.Name == name || entry.Name == plugin.Name {
308325
found = true
326+
currentPlugin = entry
309327
oldComposeFilePath = entry.ComposeFilePath
310328
break
311329
}
@@ -319,16 +337,28 @@ func updatePlugins(router *mux.Router, router_public *mux.Router) func(http.Resp
319337

320338
//if a GitURL is set, ensure OTP authentication for 'admin'
321339
if !plugin.Plus && plugin.GitURL != "" {
322-
if hasValidJwtOtpHeader("admin", r) {
323-
http.Error(w, "OTP Token invalid for Remote Install", 400)
340+
341+
check_otp := true
342+
if found {
343+
if currentPlugin.MatchesData(plugin) {
344+
//for on/off with Enabled state don't need to validate the otp
345+
check_otp = false
346+
}
347+
}
348+
349+
if check_otp && !hasValidJwtOtpHeader("admin", r) {
350+
http.Redirect(w, r, "/auth/validate", 302)
324351
return
325352
}
326353

327-
//clone but don't auto-config.
328-
ret := downloadUserExtension(plugin.GitURL, false)
329-
if ret == false {
330-
fmt.Println("Failed to download extension " + plugin.GitURL)
331-
// fall thru, dont fail
354+
//download new plugins
355+
if !found {
356+
//clone but don't auto-config.
357+
ret := downloadUserExtension(plugin.GitURL, false)
358+
if ret == false {
359+
fmt.Println("Failed to download extension " + plugin.GitURL)
360+
// fall thru, dont fail
361+
}
332362
}
333363
}
334364

@@ -693,6 +723,20 @@ func startExtension(composeFilePath string) bool {
693723
return true
694724
}
695725

726+
func restartExtension(composeFilePath string) bool {
727+
if composeFilePath == "" {
728+
//no-op
729+
return true
730+
}
731+
732+
_, err := superdRequest("restart", url.Values{"compose_file": {composeFilePath}}, nil)
733+
if err != nil {
734+
return false
735+
}
736+
737+
return true
738+
}
739+
696740
func updateExtension(composeFilePath string) bool {
697741
_, err := superdRequest("update", url.Values{"compose_file": {composeFilePath}}, nil)
698742
if err != nil {
@@ -831,11 +875,27 @@ func startExtensionServices() error {
831875
if !updateExtension(entry.ComposeFilePath) {
832876
return errors.New("Could not update Extension at " + entry.ComposeFilePath)
833877
}
834-
}
835878

836-
if !startExtension(entry.ComposeFilePath) {
837-
return errors.New("Could not start Extension at " + entry.ComposeFilePath)
879+
//if it is pfw we restart for fw rules to refresh after api
880+
if entry.Name == "PFW" {
881+
if !restartExtension(entry.ComposeFilePath) {
882+
//try a start
883+
if !startExtension(entry.ComposeFilePath) {
884+
return errors.New("Could not start Extension at " + entry.ComposeFilePath)
885+
}
886+
}
887+
} else {
888+
if !startExtension(entry.ComposeFilePath) {
889+
return errors.New("Could not start Extension at " + entry.ComposeFilePath)
890+
}
891+
}
892+
893+
} else {
894+
if !startExtension(entry.ComposeFilePath) {
895+
return errors.New("Could not start Extension at " + entry.ComposeFilePath)
896+
}
838897
}
898+
839899
}
840900
}
841901
return nil

frontend/src/components/Auth/OTPValidate.js

+29
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React, { useEffect, useState } from 'react'
22
import PropTypes from 'prop-types'
3+
import { useNavigate } from 'react-router-dom'
4+
35
import { authAPI, setJWTOTPHeader } from 'api'
46

57
import {
@@ -12,11 +14,14 @@ import {
1214
FormControlErrorText,
1315
Input,
1416
InputField,
17+
Text,
1518
VStack
1619
} from '@gluestack-ui/themed'
1720

1821
const OTPValidate = ({ onSuccess, ...props }) => {
22+
const navigate = useNavigate()
1923
const [code, setCode] = useState('')
24+
const [status, setStatus] = useState('')
2025
const [errors, setErrors] = useState({})
2126

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

4045
useEffect(() => {
46+
authAPI
47+
.statusOTP()
48+
.then((res) => {
49+
setStatus(res.State)
50+
})
51+
.catch((err) => {})
52+
4153
if (!code.length) {
4254
setErrors({})
4355
}
4456
}, [code])
4557

58+
if (status == 'unregistered') {
59+
return (
60+
<VStack space="md">
61+
<Text>Need to setup OTP auth for this feature</Text>
62+
<Button
63+
variant="outline"
64+
onPress={() => {
65+
navigate('/admin/auth')
66+
onSuccess() // only to close the modal
67+
}}
68+
>
69+
<ButtonText>Setup OTP</ButtonText>
70+
</Button>
71+
</VStack>
72+
)
73+
}
74+
4675
return (
4776
<VStack space="md">
4877
<FormControl isInvalid={'validate' in errors}>

frontend/src/components/Dashboard/WifiWidgets.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ const WifiWidget = ({
147147
color="$muted800"
148148
sx={{ _dark: { color: '$muted400' } }}
149149
>
150-
Setup Complete
150+
151151
</Text>
152152
</VStack>
153153
)}

frontend/src/components/Flow/FlowCards.js

+13-11
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,16 @@ const triggers = [
158158
}
159159
]
160160

161+
const niceDockerName = (c) => {
162+
return (c.Names[0] || c.Id.substr(0, 8)).replace(/^\//, '')
163+
}
164+
165+
const niceDockerLabel = (c) => {
166+
let name = niceDockerName(c)
167+
let ports = c.Ports.filter((p) => p.IP != '::').map((p) => p.PrivatePort) // p.Type
168+
return `${name}:${ports}`
169+
}
170+
161171
//NOTE: titles have to match FlowList.js
162172
// or they may become invisible.
163173
const actions = [
@@ -664,14 +674,6 @@ const actions = [
664674
DstPort: '8080',
665675
Dst: { IP: '1.2.3.4' }
666676
},
667-
niceDockerName: function (c) {
668-
return (c.Names[0] || c.Id.substr(0, 8)).replace(/^\//, '')
669-
},
670-
niceDockerLabel: function (c) {
671-
let name = this.niceDockerName(c)
672-
let ports = c.Ports.filter((p) => p.IP != '::').map((p) => p.PrivatePort) // p.Type
673-
return `${name}:${ports}`
674-
},
675677
getOptions: async function (name = 'DstPort') {
676678
if (name == 'Protocol') {
677679
return labelsProtocol
@@ -690,8 +692,8 @@ const actions = [
690692
.map((c) => {
691693
return {
692694
icon: ContainerIcon,
693-
label: this.niceDockerLabel(c),
694-
value: this.niceDockerName(c)
695+
label: niceDockerLabel(c),
696+
value: niceDockerName(c)
695697
}
696698
})
697699

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

709711
if (!container) {

frontend/src/components/Plugins/InstallPlugin.js

+8-10
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,16 @@ const InstallPlugin = ({ ...props }) => {
7373
})
7474
.catch((err) => {
7575
if (err.response) {
76-
err.response
77-
.text()
78-
.then((data) => {
79-
if (data.includes('Invalid JWT')) {
80-
context.error(`One Time Passcode Authentication Required, failure: ${data}`)
81-
} else {
82-
context.error(`Check Plugin URL: ${data}`)
83-
}
76+
err.response.text().then((data) => {
77+
if (data.includes('Invalid JWT')) {
78+
//context.error(`One Time Passcode Authentication Required, failure: ${data}`)
79+
//NOTE this is catched outside of here and will show the OTP modal
80+
} else {
81+
context.error(`Check Plugin URL: ${data}`)
8482
}
85-
)
83+
})
8684
} else {
87-
context.error(`API Error`, err)
85+
context.error(`API Error`, err)
8886
}
8987
})
9088

frontend/src/components/Plugins/PluginList.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const PluginListItem = ({ item, deleteListItem, handleChange, ...props }) => {
5656
size="sm"
5757
onPress={() =>
5858
navigate(
59-
'/admin/custom_plugin/' + encodeURIComponent(item.URI)
59+
`/admin/custom_plugin/${encodeURIComponent(item.URI)}/`
6060
)
6161
}
6262
>

frontend/src/layouts/Admin.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -360,11 +360,14 @@ const AdminLayout = ({ toggleColorMode, ...props }) => {
360360
.then((res) => {
361361
setFeatures([...res])
362362
setIsWifiDisabled(!res.includes('wifi'))
363-
364363
meshAPI
365364
.leafMode()
366-
.then((res) => setIsMeshNode(JSON.parse(res) === true))
367-
.catch((err) => {})
365+
.then((res) => {
366+
setIsMeshNode(JSON.parse(res) === true)
367+
})
368+
.catch((err) => {
369+
console.log(err)
370+
})
368371
})
369372
.catch((err) => {
370373
setIsWifiDisabled(true)

frontend/src/views/Mesh.js

+12-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useContext, useEffect, useRef, useState } from 'react'
22
import { AlertContext } from 'AppContext'
33
import { copy } from 'utils'
4+
import { useNavigate } from 'react-router-dom'
45

56
import {
67
Button,
@@ -19,18 +20,18 @@ import {
1920
CopyIcon
2021
} from '@gluestack-ui/themed'
2122

22-
import { RefreshCwIcon } from 'lucide-react-native'
23-
24-
import api, { wifiAPI, meshAPI, authAPI } from 'api'
25-
import APIWifi from 'api/Wifi'
26-
27-
import APIMesh from 'api/mesh'
28-
2923
import ModalForm from 'components/ModalForm'
3024
import AddLeafRouter from 'components/Mesh/AddLeafRouter'
3125
import { ListHeader, ListItem } from 'components/List'
3226
import TokenItem from 'components/TokenItem'
3327

28+
29+
import { RefreshCwIcon } from 'lucide-react-native'
30+
31+
import api, { wifiAPI, meshAPI, authAPI, setAuthReturn } from 'api'
32+
import APIWifi from 'api/Wifi'
33+
import APIMesh from 'api/mesh'
34+
3435
const Mesh = (props) => {
3536
const [leafRouters, setLeafRouters] = useState([])
3637
const [isLeafMode, setIsLeafMode] = useState([])
@@ -39,10 +40,11 @@ const Mesh = (props) => {
3940
const [ssid, setSsid] = useState('')
4041

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

4345
let alertContext = useContext(AlertContext)
4446
let refModal = useRef(null)
45-
let [meshAvailable, setMeshAvailable] = useState(true)
47+
const navigate = useNavigate()
4648

4749
const catchMeshErr = (err) => {
4850
if (err?.message == 404 || err?.message == 502) {
@@ -283,8 +285,8 @@ const Mesh = (props) => {
283285
})
284286
.catch((e) => {
285287
alertContext.error('Could not list API Tokens. Verify OTP on Auth page')
286-
//setAuthReturn('/admin/mesh')
287-
//navigate('/auth/validate')
288+
setAuthReturn('/admin/auth')
289+
navigate('/auth/validate')
288290
})
289291
}
290292

frontend/src/views/Wireguard.js

+4
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ const Wireguard = (props) => {
8989
}
9090

9191
const updateNewDomain = () => {
92+
if (!pendingEndpoint.length) {
93+
return context.error('Missing endpoint domain')
94+
}
95+
9296
let s = endpoints ? endpoints.concat(pendingEndpoint) : [pendingEndpoint]
9397
setPendingEndpoint('')
9498
setShowInput(false)

0 commit comments

Comments
 (0)