From 8e1b9b82c14d4bbeba41f86b0001473402f9909c Mon Sep 17 00:00:00 2001 From: Tiger Wang Date: Mon, 24 Apr 2023 16:10:15 -0400 Subject: [PATCH] add /v2/casaos/health/ports to get ports in use (#1023) Signed-off-by: Tiger Wang --- api/casaos/openapi.yaml | 39 +++++++++++++++++++++++++ codegen/casaos_api.go | 63 ++++++++++++++++++++++++++++++----------- go.mod | 7 ++++- go.sum | 4 +++ route/v2.go | 7 +++-- route/v2/health.go | 17 +++++++++++ service/health.go | 56 ++++++++++++++++++++++++++++++++++++ service/health_test.go | 18 ++++++++++++ 8 files changed, 191 insertions(+), 20 deletions(-) create mode 100644 service/health_test.go diff --git a/api/casaos/openapi.yaml b/api/casaos/openapi.yaml index abecc86..1da3ba1 100644 --- a/api/casaos/openapi.yaml +++ b/api/casaos/openapi.yaml @@ -47,6 +47,19 @@ paths: $ref: "#/components/responses/GetHealthServicesOK" "500": $ref: "#/components/responses/ResponseInternalServerError" + + /health/ports: + get: + tags: + - Health methods + summary: Get port in use + operationId: getHealthPorts + responses: + "200": + $ref: "#/components/responses/GetHealthPortsOK" + "500": + $ref: "#/components/responses/ResponseInternalServerError" + /file/test: get: tags: @@ -93,6 +106,17 @@ components: data: $ref: "#/components/schemas/HealthServices" + GetHealthPortsOK: + description: OK + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - properties: + data: + $ref: "#/components/schemas/HealthPorts" + schemas: BaseResponse: properties: @@ -114,3 +138,18 @@ components: items: type: string example: "casaos.service" + + HealthPorts: + properties: + tcp: + type: array + items: + type: integer + example: 80 + x-go-name: TCP + udp: + type: array + items: + type: integer + example: 53 + x-go-name: UDP diff --git a/codegen/casaos_api.go b/codegen/casaos_api.go index 846ec94..751a610 100644 --- a/codegen/casaos_api.go +++ b/codegen/casaos_api.go @@ -26,12 +26,26 @@ type BaseResponse struct { Message *string `json:"message,omitempty"` } +// HealthPorts defines model for HealthPorts. +type HealthPorts struct { + TCP *[]int `json:"tcp,omitempty"` + UDP *[]int `json:"udp,omitempty"` +} + // HealthServices defines model for HealthServices. type HealthServices struct { NotRunning *[]string `json:"not_running,omitempty"` Running *[]string `json:"running,omitempty"` } +// GetHealthPortsOK defines model for GetHealthPortsOK. +type GetHealthPortsOK struct { + Data *HealthPorts `json:"data,omitempty"` + + // Message message returned by server side if there is any + Message *string `json:"message,omitempty"` +} + // GetHealthServicesOK defines model for GetHealthServicesOK. type GetHealthServicesOK struct { Data *HealthServices `json:"data,omitempty"` @@ -51,6 +65,9 @@ type ServerInterface interface { // Test file methods // (GET /file/test) GetFileTest(ctx echo.Context) error + // Get port in use + // (GET /health/ports) + GetHealthPorts(ctx echo.Context) error // Get service status // (GET /health/services) GetHealthServices(ctx echo.Context) error @@ -72,6 +89,17 @@ func (w *ServerInterfaceWrapper) GetFileTest(ctx echo.Context) error { return err } +// GetHealthPorts converts echo context to params. +func (w *ServerInterfaceWrapper) GetHealthPorts(ctx echo.Context) error { + var err error + + ctx.Set(Access_tokenScopes, []string{""}) + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.GetHealthPorts(ctx) + return err +} + // GetHealthServices converts echo context to params. func (w *ServerInterfaceWrapper) GetHealthServices(ctx echo.Context) error { var err error @@ -112,6 +140,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL } router.GET(baseURL+"/file/test", wrapper.GetFileTest) + router.GET(baseURL+"/health/ports", wrapper.GetHealthPorts) router.GET(baseURL+"/health/services", wrapper.GetHealthServices) } @@ -119,22 +148,24 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "BwAA///xoj5w+wwAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/go.mod b/go.mod index 614a840..e3aeb11 100644 --- a/go.mod +++ b/go.mod @@ -33,15 +33,18 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 github.com/robfig/cron/v3 v3.0.1 + github.com/samber/lo v1.38.1 github.com/satori/go.uuid v1.2.0 github.com/shirou/gopsutil/v3 v3.23.2 github.com/sirupsen/logrus v1.9.0 + github.com/stretchr/testify v1.8.2 github.com/tidwall/gjson v1.14.4 go.uber.org/goleak v1.2.1 go.uber.org/zap v1.24.0 golang.org/x/crypto v0.7.0 golang.org/x/oauth2 v0.6.0 golang.org/x/sync v0.1.0 + golang.org/x/sys v0.6.0 gorm.io/gorm v1.24.6 gotest.tools v2.2.0+incompatible ) @@ -53,6 +56,7 @@ require ( github.com/bytedance/sonic v1.8.5 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect @@ -100,6 +104,7 @@ require ( github.com/pelletier/go-toml/v2 v2.0.7 // indirect github.com/perimeterx/marshmallow v1.1.4 // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/tidwall/match v1.1.1 // indirect @@ -116,9 +121,9 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.3.0 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect golang.org/x/image v0.6.0 // indirect golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect golang.org/x/text v0.8.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index 0cee091..27f1f23 100644 --- a/go.sum +++ b/go.sum @@ -255,6 +255,8 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shirou/gopsutil/v3 v3.23.2 h1:PAWSuiAszn7IhPMBtXsbSCafej7PqUOvY6YywlQUExU= @@ -324,6 +326,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4= golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= diff --git a/route/v2.go b/route/v2.go index 013a92d..4c4a1fb 100644 --- a/route/v2.go +++ b/route/v2.go @@ -71,10 +71,10 @@ func InitV2Router() http.Handler { e.Use(echo_middleware.JWTWithConfig(echo_middleware.JWTConfig{ Skipper: func(c echo.Context) bool { return c.RealIP() == "::1" || c.RealIP() == "127.0.0.1" - //return true + // return true }, ParseTokenFunc: func(token string, c echo.Context) (interface{}, error) { - claims, code := jwt.Validate(token) + claims, code := jwt.Validate(token) // TODO - needs JWT validation if code != common_err.SUCCESS { return nil, echo.ErrUnauthorized } @@ -137,6 +137,7 @@ func InitV2DocRouter(docHTML string, docYAML string) http.Handler { } }) } + func InitFile() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { filePath := r.URL.Query().Get("path") @@ -174,7 +175,7 @@ func InitDir() http.Handler { // handles only single files not folders and multiple files // if len(list) == 1 { - //filePath := list[0] + // filePath := list[0] // info, err := os.Stat(filePath) // if err != nil { diff --git a/route/v2/health.go b/route/v2/health.go index 5f7cf2c..37eee19 100644 --- a/route/v2/health.go +++ b/route/v2/health.go @@ -24,3 +24,20 @@ func (s *CasaOS) GetHealthServices(ctx echo.Context) error { }, }) } + +func (s *CasaOS) GetHealthPorts(ctx echo.Context) error { + tcpPorts, udpPorts, err := service.MyService.Health().Ports() + if err != nil { + message := err.Error() + return ctx.JSON(http.StatusInternalServerError, codegen.ResponseInternalServerError{ + Message: &message, + }) + } + + return ctx.JSON(http.StatusOK, codegen.GetHealthPortsOK{ + Data: &codegen.HealthPorts{ + TCP: &tcpPorts, + UDP: &udpPorts, + }, + }) +} diff --git a/service/health.go b/service/health.go index 754c589..e9cab09 100644 --- a/service/health.go +++ b/service/health.go @@ -1,11 +1,21 @@ package service import ( + "bufio" + "errors" + "fmt" + "os" + "strconv" + "strings" + + "github.com/samber/lo" + "github.com/IceWhaleTech/CasaOS-Common/utils/systemctl" ) type HealthService interface { Services() (map[bool]*[]string, error) + Ports() ([]int, []int, error) } type service struct{} @@ -34,6 +44,52 @@ func (s *service) Services() (map[bool]*[]string, error) { return result, nil } +func (s *service) Ports() ([]int, []int, error) { + usedPorts := map[string]map[int]struct{}{ + "tcp": {}, + "udp": {}, + } + + for _, protocol := range []string{"tcp", "udp"} { + filename := fmt.Sprintf("/proc/net/%s", protocol) + + file, err := os.Open(filename) + if err != nil { + return nil, nil, errors.New("Failed to open " + filename) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + + localAddress := fields[1] + addressParts := strings.Split(localAddress, ":") + if len(addressParts) < 2 { + continue + } + + portHex := addressParts[1] + port, err := strconv.ParseInt(portHex, 16, 0) + if err != nil { + continue + } + + usedPorts[protocol][int(port)] = struct{}{} + } + + if err := scanner.Err(); err != nil { + return nil, nil, errors.New("Error reading from " + filename) + } + } + + return lo.Keys(usedPorts["tcp"]), lo.Keys(usedPorts["udp"]), nil +} + func NewHealthService() HealthService { return &service{} } diff --git a/service/health_test.go b/service/health_test.go new file mode 100644 index 0000000..c64537a --- /dev/null +++ b/service/health_test.go @@ -0,0 +1,18 @@ +package service_test + +import ( + "testing" + + "github.com/IceWhaleTech/CasaOS/service" + "github.com/stretchr/testify/assert" +) + +func TestPorts(t *testing.T) { + service := service.NewHealthService() + + tcpPorts, udpPorts, err := service.Ports() + assert.NoError(t, err) + + assert.NotEmpty(t, tcpPorts) + assert.NotEmpty(t, udpPorts) +}