diff --git a/backend/WebUI/api_verify.go b/backend/WebUI/api_verify.go new file mode 100644 index 0000000000000000000000000000000000000000..63cce5dc5e8f3359d23f29ca2e086f6ac718fc50 --- /dev/null +++ b/backend/WebUI/api_verify.go @@ -0,0 +1,213 @@ +package WebUI + +import ( + "encoding/json" + "fmt" + "net/http" + "net/netip" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + + "github.com/free5gc/openapi/models" + smf_factory "github.com/free5gc/smf/pkg/factory" + "github.com/free5gc/util/mongoapi" + "github.com/free5gc/webconsole/backend/logger" + "github.com/free5gc/webconsole/backend/webui_context" +) + +type VerifyScope struct { + Supi string `json:"supi"` + Sd string `json:"sd"` + Sst int `json:"sst"` + Dnn string `json:"dnn"` + Ipaddr string `json:"ipaddr"` +} + +func GetSmfUserPlaneInfo() (interface{}, error) { + logger.ProcLog.Infoln("Get SMF UserPlane Info") + + webuiSelf := webui_context.GetSelf() + webuiSelf.UpdateNfProfiles() + + ctx, _, err := webuiSelf.GetTokenCtx(models.ServiceName_NSMF_OAM, models.NfType_SMF) + if err != nil { + logger.ConsumerLog.Infof("GetTokenCtx: service %v, err: %+v", models.ServiceName_NSMF_OAM, err) + } + + var jsonData interface{} + + // TODO: support fetching data from multiple SMF + if smfUris := webuiSelf.GetOamUris(models.NfType_SMF); smfUris != nil { + requestUri := fmt.Sprintf("%s/nsmf-oam/v1/user-plane-info/", smfUris[0]) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestUri, nil) + if err != nil { + logger.ProcLog.Error(err) + return jsonData, err + } + resp, err := httpsClient.Do(req) + if err != nil { + logger.ProcLog.Error(err) + return jsonData, err + } + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + logger.ProcLog.Error(closeErr) + } + }() + + json_err := json.NewDecoder(resp.Body).Decode(&jsonData) + if json_err != nil { + logger.ProcLog.Errorf("Decode Json err: %+v", err) + } + return jsonData, err + } else { + logger.ProcLog.Error("No SMF Found") + } + return jsonData, nil +} + +func getDnnStaticIpPool(snssai models.Snssai, dnn string) (netip.Prefix, error) { + var userplaneinfo smf_factory.UserPlaneInformation + + raw_info, get_err := GetSmfUserPlaneInfo() + if get_err != nil { + logger.ProcLog.Errorf("GetSmfUserPlaneInfo(): %+v", get_err) + return netip.ParsePrefix("0.0.0.0/32") + } + + tmp, err := json.Marshal(raw_info) + if err != nil { + logger.ProcLog.Errorf("Marshal err: %+v", err) + } + unmarshal_err := json.Unmarshal(tmp, &userplaneinfo) + if unmarshal_err != nil { + logger.ProcLog.Errorf("Unmarshal err: %+v", unmarshal_err) + } + + for nodeName := range userplaneinfo.UPNodes { + if nodeName == "UPF" { + // Find the UPF node + for _, snssaiupfinfo := range userplaneinfo.UPNodes[nodeName].SNssaiInfos { + // Find the Slice (snssai) + if *snssaiupfinfo.SNssai == snssai { + for _, dnnInfo := range snssaiupfinfo.DnnUpfInfoList { + // Find the DNN name + if dnnInfo.Dnn == dnn { + if len(dnnInfo.StaticPools) > 0 { + staticPoolstr := dnnInfo.StaticPools[0].Cidr + return netip.ParsePrefix(staticPoolstr) + } + // If there is no static pool, return smallest + return netip.ParsePrefix("0.0.0.0/32") + } + } + } + } + } + } + return netip.ParsePrefix("0.0.0.0/32") +} + +func VerifyStaticIP(c *gin.Context) { + logger.ProcLog.Info("Verify StaticIP") + setCorsHeader(c) + + if !CheckAuth(c) { + c.JSON(http.StatusUnauthorized, gin.H{"cause": "Illegal Token"}) + return + } + + var checkData VerifyScope + if err := c.ShouldBindJSON(&checkData); err != nil { + logger.ProcLog.Errorln(err.Error()) + c.JSON(http.StatusBadRequest, gin.H{ + "valid": false, + "cause": err.Error(), + }) + return + } + + staticIp, parse_err := netip.ParseAddr(checkData.Ipaddr) + if parse_err != nil { + logger.ProcLog.Errorln(parse_err.Error()) + c.JSON(http.StatusOK, gin.H{ + "valid": false, + "cause": parse_err.Error(), + }) + return + } + logger.ProcLog.Debugln("check IP address:", staticIp) + + snssai := models.Snssai{ + Sd: checkData.Sd, + Sst: int32(checkData.Sst), + } + + staticPool, get_pool_err := getDnnStaticIpPool(snssai, checkData.Dnn) + if get_pool_err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": get_pool_err, + "ipaddr": staticIp, + "valid": false, + "cause": get_pool_err.Error(), + }) + return + } + + // Check in Static Pool + result := staticPool.Contains(staticIp) + if !result { + c.JSON(http.StatusOK, gin.H{ + "ipaddr": staticIp, + "valid": result, + "cause": "Not in static pool: " + staticPool.String(), + }) + logger.ProcLog.Debugln("StaticIP", staticIp, ": not in static pool: "+staticPool.String()) + return + } + + // Check not used by other UE + smDataColl := "subscriptionData.provisionedData.smData" + filter := bson.M{ + "singleNssai": snssai, + "ueId": bson.D{{Key: "$ne", Value: checkData.Supi}}, // not this UE + } + smDataDataInterface, mongo_err := mongoapi.RestfulAPIGetMany(smDataColl, filter) + if mongo_err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "ipaddr": staticIp, + "valid": false, + "cause": mongo_err.Error(), + }) + } + var smDatas []models.SessionManagementSubscriptionData + if err := json.Unmarshal(sliceToByte(smDataDataInterface), &smDatas); err != nil { + logger.ProcLog.Errorf("Unmarshal smDatas err: %+v", err) + c.JSON(http.StatusInternalServerError, gin.H{}) + return + } + for _, smData := range smDatas { + if dnnConfig, ok := smData.DnnConfigurations[checkData.Dnn]; ok { + for _, ipData := range dnnConfig.StaticIpAddress { + if checkData.Ipaddr == ipData.Ipv4Addr { + msg := "StaticIP: " + checkData.Ipaddr + " has already exist!" + logger.ProcLog.Warningln(msg) + c.JSON(http.StatusOK, gin.H{ + "ipaddr": staticIp, + "valid": false, + "cause": msg, + }) + return + } + } + } + } + + // Return the result + c.JSON(http.StatusOK, gin.H{ + "ipaddr": staticIp, + "valid": result, + "cause": "", + }) +} diff --git a/backend/WebUI/routers.go b/backend/WebUI/routers.go index 9555ff9a86e3528f93425c7f9a60c4d76887160f..d513b10c746b7e55883964453286a2ef65b3911d 100644 --- a/backend/WebUI/routers.go +++ b/backend/WebUI/routers.go @@ -243,4 +243,11 @@ var routes = Routes{ "/charging-data/:chargingMethod", GetChargingData, }, + + { + "Verify StaticIP", + http.MethodPost, + "/verify-staticip", + VerifyStaticIP, + }, } diff --git a/frontend/src/pages/SubscriberCreate.tsx b/frontend/src/pages/SubscriberCreate.tsx index e968dcaa85f257b13b61983b10380eae510426f7..410cca0c836822708d6c8df39964e5fdb5c3c316 100644 --- a/frontend/src/pages/SubscriberCreate.tsx +++ b/frontend/src/pages/SubscriberCreate.tsx @@ -32,6 +32,21 @@ import { FormControlLabel, Switch, } from "@mui/material"; + +interface VerifyScope { + supi: string; + sd: string; + sst: number; + dnn: string; + ipaddr: string; +} + +interface VerifyResult { + ipaddr: string; + valid: boolean; + cause: string; +} + import { RawOff } from "@mui/icons-material"; let isNewSubscriber = false; @@ -1542,6 +1557,25 @@ export default function SubscriberCreate() { } }; + const handleVerifyStaticIp = (sd: string, sst: number, dnn: string, ipaddr: string) => { + const scope: VerifyScope = { + supi: "", + sd: sd, + sst: sst, + dnn: dnn, + ipaddr: ipaddr, + }; + axios.post("/api/verify-staticip", scope).then((res) => { + const result = res.data as VerifyResult; + console.log(result); + if (result["valid"] === true) { + alert("OK"); + } else { + alert("NO!\nCause: " + result["cause"]); + } + }); + }; + return ( <Dashboard title="Subscription"> <Card variant="outlined"> @@ -1874,7 +1908,7 @@ export default function SubscriberCreate() { <Table> <TableBody> <TableRow> - <TableCell style={{ width: "33%" }}> + <TableCell style={{ width: "20%" }}> <FormControlLabel style={{ justifyItems: "end" }} control=<Switch @@ -1899,7 +1933,7 @@ export default function SubscriberCreate() { label="Static IPv4 Address" /> </TableCell> - <TableCell style={{ width: "66%" }}> + <TableCell style={{ width: "68%" }}> <TextField label="IPv4 Address" variant="outlined" @@ -1917,6 +1951,32 @@ export default function SubscriberCreate() { onChange={(ev) => handleChangeStaticIp(ev, index, dnn)} /> </TableCell> + <TableCell style={{ width: "12%" }}> + <Button + color="secondary" + variant="contained" + // handleVerifyStaticIp = (sd: string, sst: number, dnn: string, ipaddr: string) + onClick={() => + handleVerifyStaticIp( + row.singleNssai!.sd!, + row.singleNssai!.sst!, + dnn, + row.dnnConfigurations![dnn]["staticIpAddress"]![0].ipv4Addr!, + ) + } + sx={{ + m: 2, + backgroundColor: "blue", + "&:hover": { backgroundColor: "#7496c2" }, + }} + disabled={ + row.dnnConfigurations![dnn]["staticIpAddress"] == null || + row.dnnConfigurations![dnn]["staticIpAddress"]?.length == 0 + } + > + Verify + </Button> + </TableCell> </TableRow> </TableBody> </Table> diff --git a/frontend/src/pages/UserCreate.tsx b/frontend/src/pages/UserCreate.tsx index e1c49f6ac878a480a0794f9deb480929a1facff3..f07890fe7f814828aeb8187ae69abbb5ceabac2c 100644 --- a/frontend/src/pages/UserCreate.tsx +++ b/frontend/src/pages/UserCreate.tsx @@ -15,7 +15,7 @@ import { Table, TableBody, TableCell, - TableRow + TableRow, } from "@mui/material"; import Visibility from "@mui/icons-material/Visibility"; import VisibilityOff from "@mui/icons-material/VisibilityOff"; @@ -27,7 +27,7 @@ export interface Password { export default function UserCreate() { const navigation = useNavigate(); - const [user, setUser] = useState<User>({email: "", encryptedPassword: ""}); + const [user, setUser] = useState<User>({ email: "", encryptedPassword: "" }); const [password, setPassword] = useState<Password>({}); @@ -121,7 +121,7 @@ export default function UserCreate() { onClick={handleClickShowPassword} onMouseDown={handleMouseDownPassword} > - {showPassword ? <Visibility /> : <VisibilityOff />} + {showPassword ? <Visibility /> : <VisibilityOff />} </IconButton> </InputAdornment> ), @@ -148,7 +148,7 @@ export default function UserCreate() { onClick={handleClickShowPasswordConfirm} onMouseDown={handleMouseDownPasswordConfirm} > - {showPasswordConfirm ? <Visibility /> : <VisibilityOff />} + {showPasswordConfirm ? <Visibility /> : <VisibilityOff />} </IconButton> </InputAdornment> ),