From 78b7c6ce09657e33087239faa20b3cc9e9228d6b Mon Sep 17 00:00:00 2001 From: link Date: Tue, 23 May 2023 16:39:25 +0800 Subject: [PATCH] Add zertier (#1116) --- api/casaos/openapi.yaml | 64 ++++++++++++++++ codegen/casaos_api.go | 107 ++++++++++++++++++++++----- common/constants.go | 3 +- main.go | 1 + pkg/utils/httper/zerotier.go | 78 ++++++++++++++++++++ route/v1.go | 7 +- route/v1/zerotier.go | 138 ++++++++++++++++++++++++++++++++++- route/v2/zerotier.go | 72 ++++++++++++++++++ 8 files changed, 448 insertions(+), 22 deletions(-) create mode 100644 pkg/utils/httper/zerotier.go create mode 100644 route/v2/zerotier.go diff --git a/api/casaos/openapi.yaml b/api/casaos/openapi.yaml index 6dc11cc..2f77350 100644 --- a/api/casaos/openapi.yaml +++ b/api/casaos/openapi.yaml @@ -89,6 +89,52 @@ paths: $ref: "#/components/responses/ResponseOK" "500": $ref: "#/components/responses/ResponseInternalServerError" + /zt/info: + get: + tags: + - Zerotier methods + summary: Get Zerotier info + description: |- + Get Zerotier info. + operationId: getZerotierInfo + responses: + "200": + $ref: "#/components/responses/GetZTInfoOK" + "500": + $ref: "#/components/responses/ResponseInternalServerError" + /zt/{network_id}/status: + put: + tags: + - Zerotier methods + summary: Set Zerotier network status + description: |- + Set Zerotier network status. + operationId: setZerotierNetworkStatus + parameters: + - name: network_id + in: path + description: network id + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + status: + enum: + - online + - offline + type: string + example: "online" + responses: + "200": + $ref: "#/components/responses/GetZTInfoOK" + "500": + $ref: "#/components/responses/ResponseInternalServerError" + components: securitySchemes: access_token: @@ -132,6 +178,13 @@ components: - properties: data: $ref: "#/components/schemas/HealthPorts" + GetZTInfoOK: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ZTInfo" + schemas: BaseResponse: @@ -169,3 +222,14 @@ components: type: integer example: 53 x-go-name: UDP + ZTInfo: + properties: + id: + type: string + example: "1234567890" + name: + type: string + example: "CasaOS" + status: + type: string + example: "online" \ No newline at end of file diff --git a/codegen/casaos_api.go b/codegen/casaos_api.go index fc7e874..81e8c09 100644 --- a/codegen/casaos_api.go +++ b/codegen/casaos_api.go @@ -8,10 +8,12 @@ import ( "compress/gzip" "encoding/base64" "fmt" + "net/http" "net/url" "path" "strings" + "github.com/deepmap/oapi-codegen/pkg/runtime" "github.com/getkin/kin-openapi/openapi3" "github.com/labstack/echo/v4" ) @@ -20,6 +22,12 @@ const ( Access_tokenScopes = "access_token.Scopes" ) +// Defines values for SetZerotierNetworkStatusJSONBodyStatus. +const ( + Offline SetZerotierNetworkStatusJSONBodyStatus = "offline" + Online SetZerotierNetworkStatusJSONBodyStatus = "online" +) + // BaseResponse defines model for BaseResponse. type BaseResponse struct { // Message message returned by server side if there is any @@ -38,6 +46,13 @@ type HealthServices struct { Running *[]string `json:"running,omitempty"` } +// ZTInfo defines model for ZTInfo. +type ZTInfo struct { + Id *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Status *string `json:"status,omitempty"` +} + // GetHealthPortsOK defines model for GetHealthPortsOK. type GetHealthPortsOK struct { Data *HealthPorts `json:"data,omitempty"` @@ -54,12 +69,26 @@ type GetHealthServicesOK struct { Message *string `json:"message,omitempty"` } +// GetZTInfoOK defines model for GetZTInfoOK. +type GetZTInfoOK = ZTInfo + // ResponseInternalServerError defines model for ResponseInternalServerError. type ResponseInternalServerError = BaseResponse // ResponseOK defines model for ResponseOK. type ResponseOK = BaseResponse +// SetZerotierNetworkStatusJSONBody defines parameters for SetZerotierNetworkStatus. +type SetZerotierNetworkStatusJSONBody struct { + Status *SetZerotierNetworkStatusJSONBodyStatus `json:"status,omitempty"` +} + +// SetZerotierNetworkStatusJSONBodyStatus defines parameters for SetZerotierNetworkStatus. +type SetZerotierNetworkStatusJSONBodyStatus string + +// SetZerotierNetworkStatusJSONRequestBody defines body for SetZerotierNetworkStatus for application/json ContentType. +type SetZerotierNetworkStatusJSONRequestBody SetZerotierNetworkStatusJSONBody + // ServerInterface represents all server handlers. type ServerInterface interface { // Test file methods @@ -74,6 +103,12 @@ type ServerInterface interface { // Get service status // (GET /health/services) GetHealthServices(ctx echo.Context) error + // Get Zerotier info + // (GET /zt/info) + GetZerotierInfo(ctx echo.Context) error + // Set Zerotier network status + // (PUT /zt/{network_id}/status) + SetZerotierNetworkStatus(ctx echo.Context, networkId string) error } // ServerInterfaceWrapper converts echo contexts to parameters. @@ -125,6 +160,35 @@ func (w *ServerInterfaceWrapper) GetHealthServices(ctx echo.Context) error { return err } +// GetZerotierInfo converts echo context to params. +func (w *ServerInterfaceWrapper) GetZerotierInfo(ctx echo.Context) error { + var err error + + ctx.Set(Access_tokenScopes, []string{""}) + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.GetZerotierInfo(ctx) + return err +} + +// SetZerotierNetworkStatus converts echo context to params. +func (w *ServerInterfaceWrapper) SetZerotierNetworkStatus(ctx echo.Context) error { + var err error + // ------------- Path parameter "network_id" ------------- + var networkId string + + err = runtime.BindStyledParameterWithLocation("simple", false, "network_id", runtime.ParamLocationPath, ctx.Param("network_id"), &networkId) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter network_id: %s", err)) + } + + ctx.Set(Access_tokenScopes, []string{""}) + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.SetZerotierNetworkStatus(ctx, networkId) + return err +} + // This is a simple interface which specifies echo.Route addition functions which // are present on both echo.Echo and echo.Group, since we want to allow using // either of them for path registration @@ -157,31 +221,36 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/health/logs", wrapper.GetHealthlogs) router.GET(baseURL+"/health/ports", wrapper.GetHealthPorts) router.GET(baseURL+"/health/services", wrapper.GetHealthServices) + router.GET(baseURL+"/zt/info", wrapper.GetZerotierInfo) + router.PUT(baseURL+"/zt/:network_id/status", wrapper.SetZerotierNetworkStatus) } // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/8xX3W7bRhN9lcV+30VcUKIQI0BAIBf5aRwjKGQ0LlrAMpTVckRuQu5uZ4aKVYPvXuyS", - "smhJEeykKXola3/OnHM8szO6ldrV3lmwTDK7lQjknSWIX86A34GquLxwyDR9H9a0swyWw5/K+8poxcbZ", - "9BM5G9ZIl1CruFtV06XMrm7l/xGWMpP/S7eh0u4cpa8Uwa99TNkmt9Kj84BsOga54gh2DGJAUbZte922", - "bSJzII3GB24yk9P3sk22cj4AroyG/7iiDcvjojahzi0DWlWFW4A/Izp8lLiHS9pnsoktuuCiiz4g90ij", - "v4dLcKVNerDo+L0b2e7/owYiVcSN+0D9hkDgBi3kYrEW1Okjk4MwS8ElIAhDQtm1TCTcqNpXIDMpE4mg", - "8qmt1jJjbCCRvPZhhxiNLTriw8Td48Xahw/DUMfvd+DPJ3dgxjIUEJ3uVxSiClRuRoUbWVWHtcvXF+FE", - "k38F8NnpIwF/e3MxFHCXp3sarOM5NtYGxQdDS61IORpTByH3bNrh0SbyIXijQjF8UeuH40Y5BLpBw+sP", - "IXc6BUprIJqz+wwxR03IjBJUDigT2fvxsuHSofkrpvM2lvLmPaw7p4xduv0UmzWTyan2RnODEL/AzAoh", - "RLdBrkENoobcqBcz+cQjLAFppF3lcBQzHDKRK/x8MpOCUBPwi5ksmT1laYrqy7gwXDaLhgD74htrV6fn", - "Gn4vVQWXoMu0coVLa2Vs2pnXf8wXylrAeYCfW1OUPH8+mfibsbfFTH4r2SoA/UC2/MXEEPNF1cBxwqYu", - "hKoChdeK1PRDR+rfZ9SxSXeyYGY7VuLlxbnw6FYmBxK1IQ1VpSy4hkQNXLqcxNKhyM1yCQiWBWmwCo2j", - "cUB561AYogbCI5WL3JBuiIyzlAhfgSIQK0OGw1smrs4Mv2sWAsE7Muxwff1k40bnxL78juaJcCg+OWPF", - "lWtQvDGkHebb23m3MC6K9LP98+Vi8WoBf5yMZ7FcDMfa3QqWiVwBUlckq6ehXJ0Hq7yRmTwdT8anMpFe", - "cRlrNF2aClIGip2lAN4vtEsgFuHYxrOxjJAYS/Y8l1kYDt6aoIk4vt6DMejpZPK1rnR3Lh20ujaRzx5z", - "5VDrju9RU9cK14f4B9tUQTK7km+Hy9fhXlrGdzlkJg0s2dPbPd/x1GHFX2nYTjPwiBhB1fcb99JhrVhm", - "cmFsYH6w7R2aYv5hv86AReWKgUud1sM++U0HPm5U16i/JTf25ugfozjoEMaKhuCBymnQug8WToDtO64g", - "VtyQcEsBSpfiY99rf/ooepiDRbUzI3yXfYO5/cc42AvppR41cTAvxJ8E9yeFq+v2OhwIwSjuN1jJTKar", - "p31/kOFAD7/r+pPL6ZvpyXbA2IkeflQcv3DvTQiBbkasijN0je/i9ed+2XtN9oRet38HAAD///s5WXsj", - "DgAA", + "H4sIAAAAAAAC/8xX72/bNhD9VwhuH5pBsbxk3ToB/dAfaxoUW4Ilw4bGhktLZ4mNRKp3pyRuoP99oCjb", + "sq386pqhn2xRp7v3Hnnk47WMbVFaA4ZJRtcSgUprCJqHA+C3oHLOji0yHb1zY7E1DIbdX1WWuY4Va2vC", + "j2SNG6M4g0I1b/P8aCajs2v5PcJMRvK7cFUq9HEUvlQEf7Y1ZR1cyxJtCcjaI0gUN8luS9GBKOu6Htd1", + "HcgEKEZdOmwykkfvZB2s6JwAXugYvnFGC5R3knp/emhm9oFkbqvvE8obay7oHRoGNCp3SAF/Q7T41TCs", + "y7iNZFFb+OLCV++A+4p63IXFqVIHbbJmlte+iDbXQAFEKm1erCdqXwgErtBAIqZzQZ4f6QSEngnOAEFo", + "EsrMZSDhShVlDjKSMpAIKjky+VxGjBUEkuele0OM2qQeeLdZtnBxXLofzVA0z8vkz4bLZNowpNAo3Y4o", + "ROWgXO2mdteowo2dvjp2EVVyQ8Kn+w9M+Nfr4y6BZW9scTCWJ1gZ4xj3lpaxImVpQD6F3JJpA0cdyPvk", + "200Vw6Wa3z+vY9N22hYLnazX+HFv/6enP//y7NdhX16vUTf+lSJ1dNIXS6y42mBgTa5ND+IGIkFcoeb5", + "iVvdHp2KYyCasD2Hpou0W7sZqARQLtDIFxVnFvXnpuFWuVWp30HLXrfc15tgVA2H+3GpY64QmgcYGSGE", + "8C/IVhiDKCDR6vlIPikRZoC0G9vc4m7TgxCJROH5zkgKwpiAn49kxlxSFIaoLgep5qyaVgTYbg+D2Bbh", + "YQx/ZyqHU4izMLepDQulTeint/2ZTJUxgBOXfmJ0mvHk2XBYXg1Kk47kl4LNXaJHRMuXuikxmeYV3A5Y", + "F6lQuYPgl5AH9f8j8mjCjVUwMh6VeHF8KEq0FzoBEoWmGPJcGbAViQI4swmJmUWR6NkMEAwLisEo1JYG", + "Lssbi0ITVeC20UQkmuKKSFtDgShzUATiQpNmt9uKswPNb6upQCgtabY4Hz9ZqOGV2KbvYe4Ii+Kj1Uac", + "2QrFa02xxWT1deIHBmkanptPL6bTl1P4Z2cwatpFc6eTHWEZyAtA8k1yseea2ZZgVKllJPcHw8G+DGSp", + "OGt6NJzpHEIGas6+FHi70U6BWLiwhWYD2aTEpmUPExk5d/FGO07EzfnSMYd7w+FN5+YyLuwcxnUgnz7k", + "kz5z0exHVVEonPfhd7KplGR0Jt90h8fuuzBrTg63MqkjyRZff8A0Uf2Mb7AUNmbgXWIEVaxbi5nFQrGM", + "5FQbh7z3YO7zWV9ZrwNgkdu0o5Ln2q9TufAItwvlrcSXrI2t28XjMHY8hDaiIrgnc+qYi97GcWlbTyD8", + "eSrsTICKM/GhdQM/fBBtmt6m2nAx/0m+zm3mcRRsibRU7xLxM4eLc/1G8d4DWtaAwkX2CrSIaOzRF8qz", + "vBc9jixrJDqqLMe3dLk2wJcWzyc6qcOVESurHplOuhXa79oZ2BbsZCXYHz70ZDFXpUJVAANSc3NdL7LI", + "qxMZeBfnzo+Vh1vhbebgU6URksXtYrXBbW5nYx8MxC9tMn/QNWzdBnfMqqkKJ+7SqtrZrPk3Du5jZJcj", + "dvoRYm6t7be2qm6Z8zvWV8enN9O87tDPxm5K/EXSL4MKcxnJ8GKv9WXSBbQFNhfJk9Oj10c7q0Wx0fV1", + "cNcHa2exK3S1yyo9QFuVvl4b9/vWKb61wYzrfwMAAP//9tkexLESAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/common/constants.go b/common/constants.go index 4196f24..1f5425d 100644 --- a/common/constants.go +++ b/common/constants.go @@ -3,5 +3,6 @@ package common const ( SERVICENAME = "casaos" VERSION = "0.4.4" - BODY = " " + BODY = "" + RANW_NAME = "IceWhale-RemoteAccess" ) diff --git a/main.go b/main.go index 3a8d816..95131eb 100644 --- a/main.go +++ b/main.go @@ -151,6 +151,7 @@ func main() { "/v1/recover", "/v1/other", "/v1/zt", + "/v1/test", route.V2APIPath, route.V2DocPath, route.V3FilePath, diff --git a/pkg/utils/httper/zerotier.go b/pkg/utils/httper/zerotier.go new file mode 100644 index 0000000..bd94052 --- /dev/null +++ b/pkg/utils/httper/zerotier.go @@ -0,0 +1,78 @@ +package httper + +import ( + "fmt" + "io/ioutil" + "net/http" + "strings" +) + +func ZTGet(url string) ([]byte, error) { + port, err := ioutil.ReadFile("/var/lib/zerotier-one/zerotier-one.port") + if err != nil { + return nil, err + } + + // Build the target URL + targetURL := fmt.Sprintf("http://localhost:%s%s", strings.TrimSpace(string(port)), url) + + // Create a new request + req, err := http.NewRequest("GET", targetURL, nil) + if err != nil { + return nil, err + } + + // Add the X-ZT1-AUTH header + authToken, err := ioutil.ReadFile("/var/lib/zerotier-one/authtoken.secret") + if err != nil { + return nil, err + } + req.Header.Set("X-ZT1-AUTH", strings.TrimSpace(string(authToken))) + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return respBody, nil +} +func ZTPost(url string, body string) ([]byte, error) { + port, err := ioutil.ReadFile("/var/lib/zerotier-one/zerotier-one.port") + if err != nil { + return nil, err + } + // Build the target URL + targetURL := fmt.Sprintf("http://localhost:%s%s", strings.TrimSpace(string(port)), url) + + // Create a new request + req, err := http.NewRequest("POST", targetURL, strings.NewReader(body)) + if err != nil { + return nil, err + } + + // Add the X-ZT1-AUTH header + authToken, err := ioutil.ReadFile("/var/lib/zerotier-one/authtoken.secret") + if err != nil { + return nil, err + } + req.Header.Set("X-ZT1-AUTH", strings.TrimSpace(string(authToken))) + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return respBody, nil +} diff --git a/route/v1.go b/route/v1.go index 0889c39..12be472 100644 --- a/route/v1.go +++ b/route/v1.go @@ -40,7 +40,7 @@ func InitV1Router() *gin.Engine { }) r.GET("/v1/recover/:type", v1.GetRecoverStorage) v1Group := r.Group("/v1") - r.GET("/v1/zt/*url", v1.AddZerotierToken) + r.Any("/v1/test", v1.CheckNetwork) v1Group.Use(jwt.ExceptLocalhost(func() (*ecdsa.PublicKey, error) { return external.GetPublicKey(config.CommonInfo.RuntimePath) })) { @@ -169,6 +169,11 @@ func InitV1Router() *gin.Engine { v1OtherGroup.GET("/search", v1.GetSearchResult) } + v1ZerotierGroup := v1Group.Group("/zt") + v1ZerotierGroup.Use() + { + v1ZerotierGroup.Any("/*url", v1.ZerotierProxy) + } } return r diff --git a/route/v1/zerotier.go b/route/v1/zerotier.go index 96e2946..ce336e3 100644 --- a/route/v1/zerotier.go +++ b/route/v1/zerotier.go @@ -6,10 +6,13 @@ import ( "net/http" "strings" + "github.com/IceWhaleTech/CasaOS/common" + "github.com/IceWhaleTech/CasaOS/pkg/utils/httper" "github.com/gin-gonic/gin" + "github.com/tidwall/gjson" ) -func AddZerotierToken(c *gin.Context) { +func ZerotierProxy(c *gin.Context) { // Read the port number from the file w := c.Writer r := c.Request @@ -71,3 +74,136 @@ func copyHeaders(destination, source http.Header) { } } } + +func CheckNetwork(c *gin.Context) { + //先获取所有已创建的网络 + respBody, err := httper.ZTGet("/controller/network") + if err != nil { + fmt.Println(err) + return + } + networkId := "" + address := "" + networkNames := gjson.ParseBytes(respBody).Array() + for _, v := range networkNames { + res, err := httper.ZTGet("/controller/network/" + v.Str) + if err != nil { + fmt.Println(err) + return + } + name := gjson.GetBytes(res, "name").Str + if name == common.RANW_NAME { + fmt.Println(string(res)) + networkId = gjson.GetBytes(res, "id").Str + break + } + } + if len(networkId) == 0 { + if len(address) == 0 { + address = GetAddress() + } + networkId = CreateNet(address) + } + res, err := httper.ZTGet("/network") + if err != nil { + fmt.Println(err) + return + } + joined := false + networks := gjson.GetBytes(res, "#.id").Array() + for _, v := range networks { + if v.Str == networkId { + fmt.Println("已加入网络") + joined = true + break + } + } + if !joined { + JoinAndUpdateNet(address, networkId) + } +} +func GetAddress() string { + //获取nodeId + nodeRes, err := httper.ZTGet("/status") + if err != nil { + fmt.Println(err) + return "" + } + return gjson.GetBytes(nodeRes, "address").String() +} +func JoinAndUpdateNet(address, networkId string) { + res, err := httper.ZTPost("/network/"+networkId, "") + if err != nil { + fmt.Println(err) + return + } + fmt.Println(string(res)) + if len(address) == 0 { + address = GetAddress() + } + b := `{ + "authorized": true, + "activeBridge": true, + "ipAssignments": [ + "10.147.20.1" + ] + }` + r, err := httper.ZTPost("/controller/network/"+networkId+"/member/"+address, b) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(string(r)) +} +func CreateNet(address string) string { + body := `{ + "name": "` + common.RANW_NAME + `", + "private": false, + "v4AssignMode": { + "zt": true + }, + "ipAssignmentPools": [ + { + "ipRangeStart": "10.147.20.1", + "ipRangeEnd": "10.147.20.254" + } + ], + "routes": [ + { + "target": "10.147.20.0/24" + } + ], + "rules": [ + { + "etherType": 2048, + "not": true, + "or": false, + "type": "MATCH_ETHERTYPE" + }, + { + "etherType": 2054, + "not": true, + "or": false, + "type": "MATCH_ETHERTYPE" + }, + { + "etherType": 34525, + "not": true, + "or": false, + "type": "MATCH_ETHERTYPE" + }, + { + "type": "ACTION_DROP" + }, + { + "type": "ACTION_ACCEPT" + } + ] + }` + createRes, err := httper.ZTPost("/controller/network/"+address+"______", body) + if err != nil { + fmt.Println(err) + return "" + } + return gjson.GetBytes(createRes, "id").Str +} diff --git a/route/v2/zerotier.go b/route/v2/zerotier.go new file mode 100644 index 0000000..03d8ae4 --- /dev/null +++ b/route/v2/zerotier.go @@ -0,0 +1,72 @@ +package v2 + +import ( + "fmt" + "net/http" + + "github.com/IceWhaleTech/CasaOS-Common/utils" + "github.com/IceWhaleTech/CasaOS/codegen" + "github.com/IceWhaleTech/CasaOS/common" + "github.com/IceWhaleTech/CasaOS/pkg/utils/httper" + "github.com/labstack/echo/v4" + "github.com/tidwall/gjson" +) + +func (s *CasaOS) SetZerotierNetworkStatus(ctx echo.Context, networkId string) error { + ip := `,"via":"10.147.20.256"` + status := ctx.Request().PostFormValue("status") + if status == "online" { + ip = `` + } + body := `{ + "routes": [ + { + "target": "10.147.20.0/24"` + ip + ` + } + ] + }` + res, err := httper.ZTPost("/controller/network/"+networkId, body) + if err != nil { + fmt.Println(err) + return ctx.JSON(http.StatusInternalServerError, codegen.BaseResponse{Message: utils.Ptr(err.Error())}) + } + info := codegen.GetZTInfoOK{} + via := gjson.GetBytes(res, "routes.0.via").Str + info.Id = utils.Ptr(gjson.GetBytes(res, "id").Str) + info.Name = utils.Ptr(gjson.GetBytes(res, "name").Str) + if len(via) != 0 { + info.Status = utils.Ptr("online") + } else { + info.Status = utils.Ptr("offline") + } + return ctx.JSON(http.StatusOK, info) +} +func (s *CasaOS) GetZerotierInfo(ctx echo.Context) error { + info := codegen.GetZTInfoOK{} + respBody, err := httper.ZTGet("/controller/network") + if err != nil { + return ctx.JSON(http.StatusInternalServerError, codegen.BaseResponse{Message: utils.Ptr(err.Error())}) + } + + networkNames := gjson.ParseBytes(respBody).Array() + for _, v := range networkNames { + res, err := httper.ZTGet("/controller/network/" + v.Str) + if err != nil { + fmt.Println(err) + return ctx.JSON(http.StatusInternalServerError, codegen.BaseResponse{Message: utils.Ptr(err.Error())}) + } + name := gjson.GetBytes(res, "name").Str + if name == common.RANW_NAME { + via := gjson.GetBytes(res, "routes.0.via").Str + info.Id = utils.Ptr(gjson.GetBytes(res, "id").Str) + info.Name = &name + if len(via) != 0 { + info.Status = utils.Ptr("online") + } else { + info.Status = utils.Ptr("offline") + } + break + } + } + return ctx.JSON(http.StatusOK, info) +}